C programming is a general-purpose, procedural, imperative computer programming language developed in 1972 by Dennis M. Ritchie at Bell Telephone Laboratories to develop the UNIX operating system. C is the most widely used computer language and consistently ranks among the top programming languages worldwide.
📦
16
Units
💻
80+
Code Examples
🧪
130+
Lab Exercises
🐧
Linux
GCC Platform
🎯 Why Learn C Programming?
C programming language is a MUST for students and working professionals who want to become great Software Engineers — especially in System Programming, Embedded Systems, and Operating Systems domains. Here are the key reasons to learn C:
C is the mother of all modern programming languages. Learning C makes it far easier to master C++, Java, Python, and others.
C gives you direct access to memory management — understanding pointers and memory allocation builds a rock-solid programming foundation.
It is a low-level language that interacts directly with hardware — essential for Embedded Systems, OS development, and device drivers.
C code runs nearly as fast as assembly language — critical for performance-sensitive applications.
C is used in Linux kernel, UNIX, embedded firmware, IoT devices, compilers, databases, and countless system utilities.
A strong knowledge of C is a competitive advantage in interviews for system programming, embedded, and networking roles.
📌 Facts about C
C is the most widely used System Programming Language. Most modern state-of-the-art software has been implemented using C. Here are some important facts:
C was invented to write an operating system called UNIX. The entire UNIX OS was written in C.
C is a successor of the B language, which was introduced around the early 1970s.
The language was formalized in 1988 by ANSI (American National Standards Institute) — known as ANSI C or C89.
Later standards include C99, C11, and C17, each adding features while maintaining backward compatibility.
The Linux kernel — which powers Android, servers, and supercomputers — is written almost entirely in C.
C has influenced nearly every major programming language, including C++, Java, C#, JavaScript, PHP, and Python.
Today, C consistently ranks in the top 3 most popular programming languages in the TIOBE index.
👋 Hello World in C
Every C programmer starts here. Below is the classic first program — it demonstrates the basic structure of every C program:
hello.c#include<stdio.h>intmain() {
/* My first C program */printf("Hello, World!\n");
return0;
}
#include <stdio.h> — includes the Standard Input/Output library (needed for printf)
int main() — the entry point of every C program; execution always starts here
printf(...) — prints text to the terminal; \n moves to a new line
return 0 — tells the OS the program finished successfully
⚙️ How C Code Gets Compiled
Unlike interpreted languages, C source code goes through a multi-stage compilation pipeline before it becomes an executable binary on Linux:
Source Code
hello.c
→
Pre-processor
cpp
→
Compiler
cc1
→
Assembler
as
→
Linker
ld
→
Executable
./hello
Stage
Tool
What it does
Preprocessing
cpp
Expands #include, #define, conditional macros
Compilation
cc1
Translates C source → assembly (.s)
Assembly
as
Converts assembly → object code (.o)
Linking
ld
Combines object files + libraries → final executable
🚀 Applications of C Programming
C was initially designed for system development — particularly for programs that make up the operating system. Because C produces code that runs nearly as fast as assembly language, it became the language of choice for system software:
🖥️ Operating Systems
Linux kernel, UNIX, Windows NT kernel, macOS XNU core
📟 Embedded Systems
Microcontrollers (AVR, ARM Cortex-M), IoT firmware, RTOS
Shell (bash), text editors (Vim, Emacs), compilers, linkers
📋 Complete Course Outline
Unit
Topic
Key Concepts
1
Linux Dev Environment
GCC, terminal, file system, Linux commands
2
Vim Editor for C
Normal/Insert/Command modes, navigation, workflow
3
Structure of a C Program
Compilation pipeline, memory layout, hello world
4
Data Types, Variables & Operators
Data types, sizeof, type casting, all operators
5
Control Statements
if/else, switch, nested conditions
6
Loops in C
for, while, do-while, break, continue, goto
7
Arrays & Strings
1D/2D arrays, string.h, character arrays
9
Pointers
Address, dereference, arithmetic, pointer to array
8
Functions
Declaration, call stack, recursion, scope rules
11
Functions with Pointers
Function pointers, callbacks, qsort, signal handlers
12
Structures & Unions
struct, union, enum, typedef, padding
13
Pointers and Structures
struct pointers, -> operator, arrays of structs
14
File Handling
fopen, fread, fwrite, fseek, binary files
15
Debugging & Best Practices
GDB, Valgrind, GCC warnings, code style
16
Mini Projects
Student management, file records, calculator
👥 Who Is This Tutorial For?
This tutorial is designed for anyone who wants to learn C programming from scratch — particularly on the Linux platform. It is targeted at:
Engineering students (CSE, ECE, EEE) looking to strengthen their programming foundation
Embedded systems aspirants who need C as the primary language for firmware and drivers
Software professionals transitioning to system programming or Linux development
GATE / competitive exam candidates who need solid C knowledge
Anyone who wants to learn a language that is still deeply relevant in 2026 and beyond
✅ Prerequisites
Before starting this tutorial, you should have:
A basic understanding of computer programming terminologies (what is a variable, loop, function)
Access to a Linux system — Ubuntu, Fedora, or WSL2 on Windows works perfectly
Basic familiarity with using a terminal/command prompt
No prior C experience required — this course starts from zero
Tip: If you're on Windows, install WSL2 (Windows Subsystem for Linux) — it gives you a full Ubuntu Linux environment inside Windows. Unit 1 covers the complete setup.
❓ Frequently Asked Questions
Is C Programming still relevant in 2026?
+
Absolutely. C remains the dominant language for system programming. The Linux kernel (running on 90%+ of servers and all Android devices), all major databases, embedded firmware, compilers, and network drivers are written in C. In the embedded and IoT industry, C is irreplaceable. Every Embedded Systems, RDK-B, and device driver job requires strong C skills.
Is C difficult to learn for beginners?
+
C has a small, consistent syntax — there are very few keywords and constructs to learn. The initial difficulty is understanding pointers and memory management, but this tutorial introduces those concepts gradually with visual memory diagrams. With consistent practice (30–60 min/day), beginners become productive in 6–8 weeks.
What is the difference between C and C++?
+
C is a procedural language that focuses on functions and procedures. C++ is a superset of C that adds Object-Oriented Programming (OOP), classes, templates, exceptions, and the STL. C is preferred for OS kernels, embedded firmware, and system utilities. C++ is preferred for game engines, large-scale applications, and embedded C++ (like using Arduino libraries). Learning C first gives you a massive head-start in C++.
Why learn C on Linux instead of Windows?
+
In the real world, C development happens on Linux. All servers, embedded targets, network devices, and cloud infrastructure run on Linux. The GCC compiler, GDB debugger, Valgrind memory checker, and Make build system are native Linux tools. Learning C on Linux means you're working in the exact same environment as professional system programmers. Industry tools like Buildroot, Yocto, and Linux kernel development are all Linux-native.
What jobs require C programming skills?
+
C is a required skill for: Embedded Systems Engineer, Firmware Developer, Linux Kernel Developer, Device Driver Engineer, RDK-B Gateway Developer, Network Protocol Engineer, IoT Developer, Systems Programmer, and RTOS Developer. These roles consistently offer excellent compensation, especially in the telecom, automotive, and semiconductor industries.
How long does it take to complete this course?
+
At a relaxed self-paced of 1–2 hours/day, you can complete this course in 8–10 weeks. If you study intensively (3–4 hours/day), all 14 units can be covered in 3–4 weeks. Each unit has lab exercises — completing all labs is highly recommended to build real muscle memory with the language.
Unit 1 – Linux Development Environment Setup
Before writing any C code, you need to set up your Linux development environment. This unit covers what Linux is, which distribution to use, how to install GCC, and essential commands every developer needs.
📌 Setup Syntax Quick Reference
The first workflow in Linux development is command-driven. You create a workspace, edit a source file, compile it with GCC, and run the resulting binary from the same shell session.
mkdir -p ~/c-course/unit1
cd ~/c-course/unit1
vim hello.c
gcc -Wall -Wextra hello.c -o hello
./hello
📖 Theory Focus
Linux is preferred for C because the compiler, debugger, linker, headers, and shell workflow are all native parts of the platform.
A C developer works with files and processes directly, so understanding paths, permissions, and package tools is part of the language environment itself.
The shell is not separate from development; it is the control surface for compiling, testing, debugging, and automation.
🐧 1.1 – What is Linux & Why Use It for C?
Linux is an open-source, Unix-like operating system kernel created by Linus Torvalds in 1991. It is the dominant platform for systems programming, servers, embedded systems, and scientific computing.
Why Linux for C Development?
GCC (GNU Compiler Collection) is natively available — compiles C directly
POSIX-compliant — programs work across Linux, macOS, BSD
Full access to system calls (fork, exec, signals, sockets)
Professional tools: GDB, Valgrind, Make, Git — all built-in or one command away
No licensing cost — free and open source
Linux Distribution Comparison:
Distribution
Package Manager
Best For
Install GCC
Ubuntu
apt
Beginners, desktops
sudo apt install gcc
Fedora
dnf
Developers, cutting edge
sudo dnf install gcc
Debian
apt
Stability, servers
sudo apt install gcc
Arch Linux
pacman
Advanced users
sudo pacman -S gcc
CentOS/RHEL
yum/dnf
Enterprise servers
sudo yum install gcc
⚙️ 1.2 – Installing Required Tools
On Ubuntu/Debian (most common for beginners):
# Step 1: Update your package list
sudo apt update
# Step 2: Upgrade existing packages
sudo apt upgrade -y
# Step 3: Install build-essential# This installs: gcc, g++, make, libc-dev, and more
sudo apt install build-essential -y
# Step 4: Install additional tools
sudo apt install gdb valgrind vim -y
# Step 5: Verify installations
gcc --version # should show: gcc (Ubuntu ...) 11.x.x
make --version # should show: GNU Make 4.x
gdb --version # should show: GNU gdb ...Terminal
Expected output of gcc --version: gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc.
📁 1.3 – Linux File System Hierarchy
Everything in Linux is a file. The filesystem starts at / (root):
# Create project structure
mkdir -p ~/c-course/unit1
cd ~/c-course/unit1
# Create your first C file
touch hello.c
# Edit with nano (beginners)
nano hello.c
# OR edit with vim (recommended)
vim hello.c
# Compile
gcc hello.c -o hello
# Run
./hello
# Compile with warnings (ALWAYS do this)
gcc -Wall -Wextra -o hello hello.c
# Check binary info
file hello # ELF 64-bit LSB executable, x86-64 ...
./hello # run it
echo $? # print exit code (0 = success)Terminal
Unit 1 – Lab Exercises (10 Tasks)
Install GCC on your Linux machine. Run gcc --version and which gcc. Record the output. Also run gcc --version vs cc --version — are they different?
Create the directory structure: ~/c-course/unit1/. Inside, create 3 empty files: hello.c, notes.txt, test.c. Use only terminal commands.
Run ls -la ~/c-course/unit1/. Identify the permission bits for each file. What are the default permissions? Run umask and explain how it relates to the defaults.
Practice: use cp, mv, rm, and cat on your files. Create a text file with echo "My first file" > first.txt, display it, append a second line with >>, then display again.
Run grep -n "include" /usr/include/stdio.h | head -10. How many lines contain "include"? Try grep -c "include" /usr/include/stdio.h to get just the count.
Navigate to /usr/include/ and list the contents. Find stdio.h. Use head -30 stdio.h to inspect it. Identify two function declarations you recognize.
Write a simple shell script build.sh that compiles hello.c and runs it: #!/bin/bash gcc hello.c -o hello && ./hello Make it executable with chmod +x build.sh and run it.
Use find ~/c-course -name "*.c" to find all C files. Use file hello (after compiling) to see what type of file the executable is. Record the output.
Run man gcc and search for the -Wall option (press / then type -Wall). Write down what -Wall, -Wextra, and -Werror do.
Use ldd on a compiled binary: ldd ./hello. List all shared libraries it depends on. Find where libc.so lives on disk using ldconfig -p | grep libc.
Hint: Use Tab for autocomplete in the terminal. Use Ctrl+C to abort a running command. Use history to see previous commands.
Unit 2 – Using Vim Editor for C Programming
Vim is a powerful, keyboard-driven text editor preinstalled on virtually every Linux system. Mastering Vim dramatically increases your coding speed and is an essential skill for Linux developers.
⌨️ Vim Syntax Quick Reference
Vim commands are mode-based. You enter Insert mode to type code, return to Normal mode for navigation and editing, then use Command mode for save, search, and file actions.
vim main.c
i // enter insert mode
Esc // return to normal mode
:wq // save and quit
/printf // search forward
gg G :25 // top, bottom, line 25
📖 Theory Focus
Vim is modal because editing, movement, and commands are treated as different operations with different optimised key sets.
Once navigation becomes muscle memory, you spend less time reaching for the mouse and more time reasoning about code structure.
For C development on servers or embedded targets, Vim matters because it is available almost everywhere, even in minimal environments.
🔄 2.1 – Vim's Three Core Modes
Vim is modal — it behaves differently depending on which mode you're in. This is the single most important concept to understand:
NORMAL MODE
Default mode Navigation & commands
INSERT MODE
Press i to enter Type your code
COMMAND MODE
Press : to enter Save, quit, search
VISUAL MODE
Press v to enter Select text
NORMAL → INSERT: i (before cursor) a (after) o (new line)
INSERT → NORMAL: Esc
NORMAL → COMMAND: :
NORMAL → VISUAL: v (char) V (line) Ctrl+v (block)
Golden Rule: When in doubt, press Esc to return to Normal mode. You can always start over from there.
🧭 2.2 – Navigation in Normal Mode
Key(s)
Action
Key(s)
Action
h j k l
Left / Down / Up / Right
w / b
Next / previous word
0 / $
Start / end of line
gg / G
Top / bottom of file
:n
Go to line n (e.g. :25)
Ctrl+d / Ctrl+u
Scroll half page down/up
%
Jump to matching bracket
*
Search word under cursor
✏️ 2.3 – Editing Commands (Normal Mode)
Command
Action
dd
Delete (cut) current line
yy
Yank (copy) current line
p / P
Paste after / before cursor
u
Undo
Ctrl+r
Redo
x
Delete character under cursor
r
Replace one character
cw
Change word (delete word and enter insert mode)
D
Delete from cursor to end of line
=G
Auto-indent entire file from cursor to end
/pattern
Search forward for pattern
:%s/old/new/g
Replace all "old" with "new" in file
💾 2.4 – Saving and Exiting
:w # Save (write) the file
:q # Quit (only works if no unsaved changes)
:wq # Save AND quit (most common)
:wq! # Force save and quit
:q! # Discard changes and quit (force quit)
:x # Same as :wq (save if modified, then quit)
ZZ # In normal mode: save and quit
ZQ # In normal mode: quit without saving
:w newname.c # Save as new filenameVim Command Mode
🔧 2.5 – Configuring Vim for C (.vimrc)
Create ~/.vimrc to customize Vim for C development:
" ~/.vimrc - Vim config for C programming
syntax on " Enable syntax highlighting
set number " Show line numbers
set tabstop=4 " Tab = 4 spaces
set shiftwidth=4 " Indent = 4 spaces
set expandtab " Convert tabs to spaces
set autoindent " Auto-indent new lines
set smartindent " Smarter indentation
set hlsearch " Highlight search results
set incsearch " Incremental search
set showmatch " Highlight matching brackets
set ruler " Show cursor position
set colorcolumn=80 " Column guide at 80 chars
set background=dark
colorscheme desert " Color scheme~/.vimrc
Apply changes:source ~/.vimrc or restart Vim
🚀 2.6 – Complete Vim Workflow: Writing & Running a C Program
# Step 1: Open a new C file in vim
vim hello.c
Terminal
// Step 2: You are now in NORMAL mode. Press 'i' to enter INSERT mode
// Now type your code:#include<stdio.h>intmain() {
printf("Hello, Linux C!\n");
return0;
}
hello.c
# Step 3: Press Esc to return to NORMAL mode
# Step 4: Type :wq and press Enter to save and quit
# Step 5: Compile and run
gcc hello.c -o hello
./hello
Hello, Linux C!
Terminal
Workflow Summary: 1. vim filename.c → opens Vim in NORMAL mode
2. Press i → switch to INSERT mode
3. Type your code
4. Press Esc → back to NORMAL mode
5. Type :wq → saves and exits
Unit 2 – Lab Exercises (10 Tasks)
Open Vim and practice switching between Normal, Insert, and Command modes at least 5 times. Get comfortable with Esc. Practice the full cycle: Normal → Insert → type text → Esc → Command → save.
Create greet.c in Vim. Write a program that prints "Welcome to Linux C Programming!" then compile and run it. Use only keyboard shortcuts — no mouse.
Open any .c file in Vim. Practice navigation: use gg, G, w, b, 0, $, and :25 to move around. Time yourself navigating to line 10, last line, first word, and last word of a line.
Create ~/.vimrc with the configuration shown above (syntax, line numbers, tab=4, etc.). Reopen a C file and verify syntax highlighting and line numbers are active.
In Vim, use the search command /printf to find all occurrences of printf in a file. Use n to jump to next and N for previous. Count total occurrences using :%s/printf//gn.
Use :%s/hello/world/g to do a global replacement in a test file. Verify all occurrences changed. Then undo with u to restore. Redo with Ctrl+r.
Practice Vim editing commands: use dd to delete a line, yy to copy it, p to paste below, P to paste above. Practice on a 5-line test file.
In Vim, select 3 lines using Visual Line mode (V), then indent them with > and unindent with <. Practice indenting a whole file with gg=G.
Open two files simultaneously in split windows in Vim using :split file2.c. Navigate between splits with Ctrl+w w. Copy a block from one file to the other using yank and paste.
Create a file with 20 lines. Use Vim macros: press qa to record a macro into register 'a', perform an operation (e.g., add a semicolon at end of line and move down), press q to stop. Then press 18@a to repeat for the remaining 18 lines.
Hint: If you get stuck in Vim, always press Esc first, then :q! to exit without saving. Type :help for the built-in documentation.
Unit 3 – Structure of a C Program
Every C program follows a well-defined structure. This unit explains each part, shows how GCC turns your source code into an executable, and reveals exactly how a C program looks in memory at runtime.
🧱 Program Syntax Quick Reference
C source files usually combine preprocessor directives, declarations, function prototypes, main, and function definitions in a predictable layout.
#include<stdio.h>inthelper(int value);
intmain(void) {
int result = helper(5);
return0;
}
inthelper(int value) {
return value * 2;
}
📖 Theory Focus
The preprocessor runs before the compiler, so directives such as #include and #define transform the source before syntax analysis begins.
main is the language entry point, but your executable also depends on linking, startup code, libraries, and OS process loading.
Program structure is not only stylistic; it determines symbol visibility, memory placement, and whether the compiler knows a function signature before use.
🏗️ 3.1 – Anatomy of a C Program
/*─────────────────────────────────────────────
SECTION 1: Preprocessor Directives
Processed BEFORE compilation by the C preprocessor.
#include pulls in header files.
#define creates constants/macros.
─────────────────────────────────────────────*/#include<stdio.h>// standard I/O: printf, scanf, fopen ...#include<stdlib.h>// malloc, free, exit, atoi ...#include<string.h>// strlen, strcpy, strcmp ...#define MAX 100// symbolic constant#define PI 3.14159// no semicolon on #define!/*─────────────────────────────────────────────
SECTION 2: Global Declarations
Visible to ALL functions in the file.
Stored in the Data/BSS segment (not stack).
─────────────────────────────────────────────*/int globalCounter = 0; // initialized → Data segmentfloat ratio; // uninitialized → BSS segment (0)/*─────────────────────────────────────────────
SECTION 3: Function Prototypes (declarations)
Tell the compiler the function exists before it's defined.
─────────────────────────────────────────────*/voidgreet(char *name);
intadd(int a, int b);
/*─────────────────────────────────────────────
SECTION 4: main() – Entry Point
Every C program must have exactly ONE main().
Returns int: 0 = success, non-zero = error.
─────────────────────────────────────────────*/intmain() {
/* Local variables → stored on the STACK */int result;
char name[50] = "Alice";
greet(name); // function call
result = add(5, 3);
printf("5 + 3 = %d\n", result);
return0; // signals success to OS
}
/*─────────────────────────────────────────────
SECTION 5: Function Definitions
─────────────────────────────────────────────*/voidgreet(char *name) {
printf("Hello, %s!\n", name);
}
intadd(int a, int b) {
return a + b;
}
program.c
🗂️ 3.1 – Elements of a C Program Structure
#
Element
Where It Goes
Purpose
Example
1
Preprocessor Directive
Top of file, before everything else
Instructs the preprocessor — includes headers, defines constants/macros. Processed before compilation.
#include <stdio.h> #define MAX 100
2
Global Variable
Outside all functions, after directives
A variable accessible by all functions in the file. Lives in the Data segment (initialized) or BSS segment (uninitialized).
int counter = 0; float ratio;
3
Variable Declaration
Inside a function, before first use
Tells the compiler the type and name of a variable. Memory is reserved on the stack. May not have an initial value.
int age; char name[50];
4
Function Declaration(Prototype)
After global variables, before main()
Tells the compiler the function's name, return type, and parameter types — so it can be called before its definition appears.
int add(int a, int b); void greet(char *name);
5
Variable Definition
Inside a function (or globally)
A declaration that also reserves storage. Assigning a value at this point is called initialization.
int age = 25; float pi = 3.14f;
6
Function Definition
After main(), or in separate .c files
The full body of the function — the actual implementation. Contains the logic that runs when the function is called.
int add(int a, int b) { return a + b; }
Declaration vs Definition: A declaration introduces a name and type to the compiler. A definition also allocates memory or provides the function body. Every definition is a declaration, but not every declaration is a definition (e.g., a prototype is a declaration only).
⚙️ 3.2 – The Compilation Pipeline (Source → Executable)
GCC transforms your C source file into a runnable executable in four stages:
Source Code
hello.c
→
1. Preprocessor
cpp
→
Modified Source
hello.i
→
2. Compiler
cc1
→
Assembly Code
hello.s
→
3. Assembler
as
→
Object File
hello.o
→
4. Linker
ld
→
Executable
./hello
Stage
Tool
Input
Output
What It Does
1. Preprocessing
cpp
.c
.i
Expands #include, #define, removes comments
2. Compilation
cc1
.i
.s
Converts C to assembly language
3. Assembly
as
.s
.o
Converts assembly to machine code (binary)
4. Linking
ld
.o + libs
executable
Combines object files + standard libraries
# Run each stage separately to see intermediate files:
gcc -E hello.c -o hello.i # Stage 1: Preprocessing only
gcc -S hello.c -o hello.s # Stage 2: Compile to assembly
gcc -c hello.c -o hello.o # Stage 3: Compile to object file
gcc hello.o -o hello # Stage 4: Link to executable# OR do everything in one command (normal usage):
gcc hello.c -o hello
# View the .i file (preprocessed - stdio.h included inline):
cat hello.i | head -50
# View the assembly (.s file):
cat hello.s
Terminal
🗺️ 3.3 – C Program Memory Layout (Runtime)
When your program runs, the OS loads it into memory. The process memory is divided into distinct segments:
High Address0xFFFF…FFFF
Command Line Args & Environment Vars
argv[], envp[]
High
Stack ↓
Local vars, parameters, return addresses Function call frames — grows DOWNWARD
Stack overflow occurs when too many function calls exhaust the stack space (common with deep recursion).
Memory leak occurs when heap memory allocated with malloc() is never freed.
📝 3.4 – Hello World – Every Line Explained
#include<stdio.h>// ↑ Preprocessor directive.
// Tells the preprocessor to copy the contents of
// /usr/include/stdio.h into this file before compiling.
// stdio.h declares printf(), scanf(), FILE, etc.intmain() {
// ↑ Every C program starts execution here.
// int → the function returns an integer to the OS.
// () → no parameters (same as (void))printf("Hello, Linux C!\n");
// ↑ printf is defined in stdio.h.
// "Hello, Linux C!\n" is a string literal (in Text segment).
// \n is an escape sequence for newline.return0;
// ↑ Returns 0 to the OS/shell.
// Convention: 0 = success, any other value = error.
// You can check this with: echo $? (after running)
}
// ↑ Closing brace ends the main() function body.hello.c – annotated
🔤 3.5 – Escape Sequences & Format Specifiers
Escape Seq.
Meaning
ASCII Value
\n
Newline (moves to next line)
10
\t
Horizontal tab
9
\\
Backslash character
92
\"
Double quote
34
\'
Single quote
39
\0
Null character (string terminator)
0
\r
Carriage return
13
Format Spec.
Type
Example
%d
int (decimal)
printf("%d", 42) → 42
%f
float/double
printf("%.2f", 3.14) → 3.14
%c
char
printf("%c", 'A') → A
%s
string (char*)
printf("%s", "hi") → hi
%p
pointer address
printf("%p", ptr) → 0x7ffe…
%ld
long int
printf("%ld", 1000000L)
%lu
unsigned long
printf("%lu", sizeof(int))
%x
hexadecimal
printf("%x", 255) → ff
Unit 3 – Lab Exercises
Write a program that prints: your name, your age, and today's date — each on a separate line using printf.
Run gcc -E hello.c -o hello.i then open hello.i in Vim. How many lines does it have? What happened to your #include <stdio.h>?
Run gcc -S hello.c -o hello.s and view the assembly output. Find the call printf instruction in the .s file.
Write a program with a global initialized variable (int g = 42;), a global uninitialized variable (int h;), and a local variable. Print all three with printf and their addresses with %p. Observe which addresses are close together.
Write a program using printf with at least 5 different format specifiers (%d %f %c %s %x). Use proper width specifiers like %10d for alignment.
Modify the Hello World program to accept a name from user input using scanf and print "Hello, [name]!".
Hint: Check the return value of main() with echo $? in the terminal after running your program.
📐 3.6 – GCC Compilation Flags Cheat Sheet
Flag
Purpose
Example
-o name
Name the output file
gcc hello.c -o hello
-Wall
Enable all common warnings
gcc -Wall hello.c -o hello
-Wextra
Enable extra warnings
gcc -Wall -Wextra hello.c -o hello
-g
Include debug info (for GDB)
gcc -g hello.c -o hello
-O2
Optimize for speed
gcc -O2 hello.c -o hello
-std=c99
Use C99 standard
gcc -std=c99 hello.c -o hello
-E
Preprocess only
gcc -E hello.c -o hello.i
-S
Compile to assembly
gcc -S hello.c -o hello.s
-c
Compile to object file
gcc -c hello.c -o hello.o
Unit 3 – Lab Exercises (10 Tasks)
Write a program that prints your name, age, and today's date — each on a separate line using printf. Then add a header line "==[ My Profile ]==" for formatting.
Run gcc -E hello.c -o hello.i then open hello.i in Vim. Count its lines with wc -l hello.i. What happened to your #include <stdio.h>? Find the line that became your own code.
Run gcc -S hello.c -o hello.s and view the assembly output. Find the call printf instruction (or call puts if GCC optimized). Count total lines in the assembly file.
Write a program with a global initialized variable (int g = 42;), a global uninitialized variable (int h;), and a local variable. Print all three with printf and their addresses using %p. Observe which addresses are in similar ranges.
Write a program using printf with all 8 format specifiers from section 3.5 (%d %f %c %s %p %ld %lu %x). Use proper width and precision specifiers like %10.2f and %-20s for aligned output.
Modify Hello World to accept a name from user input using scanf("%49s", name) (note: 49 to prevent buffer overrun) and print "Hello, [name]!" safely.
Write a program that demonstrates all 7 escape sequences from the table in section 3.5. Print each with a label showing what it does visually (newlines, tabs, quotes, etc.).
Compile the same program with three different GCC flags: (a) gcc hello.c -o hello, (b) gcc -O2 hello.c -o hello_opt, (c) gcc -g hello.c -o hello_debug. Compare file sizes with ls -la hello*.
Write a program that intentionally has a warning (e.g., unused variable int x;). Compile with no flags — no warning shown. Then compile with -Wall — warning appears. Then with -Werror — compilation fails. Record what each flag does.
Write a multi-file program: create math_utils.c with a function int square(int n) { return n*n; } and math_utils.h with its declaration. In main.c, include the header and call square(5). Compile using gcc main.c math_utils.c -o prog and run it.
Hint: Always compile with gcc -Wall -Wextra -o prog prog.c as a best practice. Treat all warnings as errors to be fixed.
Unit 4 – Data Types, Variables & Operators
This unit begins with the fundamental data types C provides and how each occupies memory, then moves to how variables are declared, initialized, and scoped, and finally covers all the operators used to build expressions.
🧮 Data Types → Variables → Operators
Every C program is built from typed data. First choose the right type, then declare a variable to hold it, then apply operators to compute and compare values.
/* 1. Data type determines size & range */int age = 21;
float price = 99.5f;
constint MAX = 100;
/* 2. Variable holds the value */int total, count = 0;
/* 3. Operators compute expressions */
average = (float)sum / count;
is_even = (age % 2 == 0);
📖 Theory Focus
Data Types define the size, range, and behaviour of stored values — choosing the wrong type causes overflow or wasted memory.
Variables are named memory locations whose type determines how the stored bits are interpreted at runtime.
Operators do more than arithmetic — they define precedence, type promotion rules, and can produce undefined behaviour if misused.
🧮 4.1 – Fundamental Data Types & Memory Sizes
On a 64-bit Linux system (x86-64), data types have these sizes:
A variable declaration tells the compiler to reserve memory and associate a name with it.
#include<stdio.h>intmain() {
/* ── Declaration (memory reserved, value undefined) ── */int age;
float salary;
char grade;
double pi;
/* ── Initialization (declaration + assignment) ── */int count = 0;
float price = 99.99f; // 'f' suffix = float literalchar letter = 'A';
double area = 3.14159;
/* ── Constants (value cannot change) ── */constint MAX_SIZE = 100;
constfloat TAX_RATE = 0.18f;
// MAX_SIZE = 200; // ERROR: cannot modify const/* ── Multiple variables on one line ── */int x = 1, y = 2, z = 3;
/* ── Print addresses (where in memory) ── */printf("age : %lu bytes at address %p\n", sizeof(age), &age);
printf("salary : %lu bytes at address %p\n", sizeof(salary), &salary);
printf("grade : %lu bytes at address %p\n", sizeof(grade), &grade);
return0;
}
variables.c
Memory addresses on stack are typically adjacent. On a 64-bit Linux system, local variables are allocated in the stack frame and addresses may show a pattern (e.g., each 4 bytes apart for int).
📝 Topic Assignments — 4.2 Variables
Declare variables of 5 different data types, initialize them, and print both value and address.
Create a program that uses const values for tax and discount, then computes final price.
Read student details (name, age, marks) and store them in correctly typed variables.
🔄 4.4 – Type Casting (Implicit & Explicit)
#include<stdio.h>intmain() {
int a = 5, b = 2;
double result;
/* ── Implicit (automatic) conversion ── */
result = a / b; // INTEGER division: 5/2 = 2 (not 2.5!)printf("int/int = %.1f\n", result); // 2.0/* ── Explicit (manual) cast ── */
result = (double)a / b; // Cast 'a' to double FIRST, then divideprintf("(double)/int= %.1f\n", result); // 2.5/* ── Char ↔ int (ASCII values) ── */char c = 'A';
printf('%c' has ASCII value %d\n", c, (int)c); // 65printf("ASCII 66 is char '%c'\n", (char)66); // 'B'/* ── Float to int truncates (does NOT round) ── */float f = 3.99f;
printf("(int)3.99 = %d\n", (int)f); // 3 (truncated!)return0;
}
casting.c
📝 Topic Assignments — 4.4 Type Casting
Show difference between 5/2 and (double)5/2 in one program.
Convert a lowercase character to uppercase using explicit cast and ASCII arithmetic.
Read a float and print its truncated integer part using explicit cast.
🖊️ 4.5 – Assignment Operators
The assignment operator= stores a value into a variable. C also provides compound assignment operators that combine an arithmetic or bitwise operation with assignment, making code shorter and more readable.
Operator
Symbol
Equivalent to
Example
Result (x=10)
Simple Assignment
=
—
x = 5
x = 5
Add & Assign
+=
x = x + n
x += 3
x = 13
Subtract & Assign
-=
x = x - n
x -= 4
x = 6
Multiply & Assign
*=
x = x * n
x *= 2
x = 20
Divide & Assign
/=
x = x / n
x /= 2
x = 5
Modulo & Assign
%=
x = x % n
x %= 3
x = 1
Bitwise AND & Assign
&=
x = x & n
x &= 0xFF
low byte of x
Bitwise OR & Assign
|=
x = x | n
x |= 0x01
sets bit 0
Bitwise XOR & Assign
^=
x = x ^ n
x ^= 0xFF
flips all bits
Left Shift & Assign
<<=
x = x << n
x <<= 2
x = 40
Right Shift & Assign
>>=
x = x >> n
x >>= 1
x = 5
#include<stdio.h>intmain() {
int x = 10;
/* ── Simple Assignment ── */
x = 10;
printf("x = 10 → x = %d\n", x); // 10/* ── Compound Assignment ── */
x += 5; printf("x += 5 → x = %d\n", x); // 15
x -= 3; printf("x -= 3 → x = %d\n", x); // 12
x *= 2; printf("x *= 2 → x = %d\n", x); // 24
x /= 4; printf("x /= 4 → x = %d\n", x); // 6
x %= 4; printf("x %%= 4 → x = %d\n", x); // 2/* ── Chained Assignment (right to left) ── */int a, b, c;
a = b = c = 100; // c=100, b=c=100, a=b=100 (R→L)printf("a=%d b=%d c=%d\n", a, b, c); // 100 100 100/* ── Bitwise compound ── */int flags = 0b00000000;
flags |= (1 << 3); // set bit 3: 0b00001000 = 8
flags |= (1 << 5); // set bit 5: 0b00101000 = 40
flags &= ~(1 << 3); // clear bit 3:0b00100000 = 32printf("flags = %d\n", flags); // 32/* ── CAUTION: = vs == ── */int y = 5;
if (y == 5) printf("y equals 5\n"); // comparison (==)// if (y = 5) — ALWAYS true! Assigns 5, not compares!return0;
}
assignment_ops.c
📐 How Assignment Works in Memory
Before: x = 10
10
x (addr 0x1000)
→
After: x += 5
15
x (same addr 0x1000)
Key: Assignment writes a new value into the same memory cell. Compound operators (+=, -=…) read then overwrite the same address.
📋 Program Output
► assignment_ops.c
x = 10 → x = 10
x += 5 → x = 15
x -= 3 → x = 12
x *= 2 → x = 24
x /= 4 → x = 6
x %= 4 → x = 2
a=100 b=100 c=100
flags = 32
y equals 5
🔍 Result Analysis:
x += 5 → 15: reads x (10), adds 5, writes 15 back. Each compound step chains on the updated x.
a = b = c = 100: Right-to-left — c gets 100 first, then b=c, then a=b. All three become 100.
flags = 32: Set bits 3 (8) and 5 (32) → 40, then clear bit 3 → 40−8 = 32.
y equals 5: y == 5 is a comparison returning 1 (true), so the branch executes.
= vs == is one of the most common bugs in C. = assigns a value; == tests equality. Writing if (x = 5) instead of if (x == 5) always evaluates to true and silently changes x!
📝 Topic Assignments — 4.5 Assignment Operators
Starting with x=25, apply +=, -=, *=, /=, and %= step by step and print each result.
Demonstrate right-to-left evaluation using a=b=c=50 and explain order.
Implement a flag register using |=, &=~, and ^=.
➕ 4.6 – Arithmetic Operators
Arithmetic operators perform mathematical calculations. All five work on integer and floating-point types, but behave differently for each — especially / and %.
Operator
Symbol
Name
Example (a=10, b=3)
Result
Addition
+
Binary plus
a + b
13
Subtraction
-
Binary minus
a - b
7
Multiplication
*
Multiply
a * b
30
Division
/
Divide
a / b
3 (integer!)
Modulo
%
Remainder
a % b
1
Unary minus
-
Negate
-a
-10
#include<stdio.h>intmain() {
int a = 10, b = 3;
double x = 10.0, y = 3.0;
/* ── Integer arithmetic ── */printf("=== Integer (a=10, b=3) ===\n");
printf("a + b = %d\n", a + b); // 13printf("a - b = %d\n", a - b); // 7printf("a * b = %d\n", a * b); // 30printf("a / b = %d\n", a / b); // 3 ← TRUNCATED (not 3.33)printf("a %% b = %d\n", a % b); // 1 (10 = 3×3 + 1)/* ── Float arithmetic ── */printf("\n=== Double (x=10.0, y=3.0) ===\n");
printf("x / y = %.4f\n", x / y); // 3.3333printf("x * y = %.4f\n", x * y); // 30.0000/* ── Integer division pitfall ── */printf("\n=== Conversion needed ===\n");
printf("5 / 2 = %d\n", 5 / 2); // 2 !printf("5.0 / 2 = %.1f\n", 5.0 / 2); // 2.5printf("(double)5/2 = %.1f\n", (double)5 / 2); // 2.5 (cast)/* ── Modulo applications ── */printf("\n=== Modulo uses ===\n");
printf("17 %% 5 = %d\n", 17 % 5); // 2 (remainder)printf("Is 18 even? %s\n", (18 % 2 == 0) ? "Yes" : "No"); // Yesprintf("Hour wrap: %d\n", (23 + 3) % 24); // 2 (circular clock)/* ── Unary minus and plus ── */int n = 7;
printf("-n = %d\n", -n); // -7printf("+n = %d\n", +n); // 7 (no effect, clarifies intent)/* ── Overflow example ── */int big = 2147483647; // INT_MAXprintf("INT_MAX + 1 = %d\n", big + 1); // -2147483648 (overflow!)return0;
}
arithmetic.c
📐 Integer vs Float Division
❌ int / int — truncates
10 / 3
= 3 (.333 discarded — not rounded!)
✅ double / int — preserves fraction
(double)10 / 3
= 3.3333
% Modulo — remainder only
10 % 3
10 = 3×3 + 1 → result = 1
📋 Program Output
► arithmetic.c
=== Integer (a=10, b=3) ===
a + b = 13
a - b = 7
a * b = 30
a / b = 3
a % b = 1
=== Double (x=10.0, y=3.0) ===
x / y = 3.3333
x * y = 30.0000
=== Conversion needed ===
5 / 2 = 2
5.0 / 2 = 2.5
(double)5/2 = 2.5
=== Modulo uses ===
17 % 5 = 2
Is 18 even? Yes
Hour wrap: 2
-n = -7
+n = 7
INT_MAX + 1 = -2147483648
🔍 Result Analysis:
a / b = 3 not 3.33 — both are int; C truncates toward zero.
5.0 / 2 = 2.5 — literal 5.0 makes one operand double, promoting the whole expression.
Hour wrap: 2 — (23+3) % 24 = 26 % 24 = 2. Modulo is the standard wrap-around idiom.
INT_MAX + 1 = −2147483648 — signed integer overflow wraps to the most negative value (undefined behaviour — avoid it).
Key Rules: (1) int / int always gives an integer — fractional part is discarded, not rounded. (2) % only works on integer types, not float/double. (3) Use double or cast when you need decimal precision.
📝 Topic Assignments — 4.6 Arithmetic Operators
Create a calculator for +, -, *, /, and % with division-by-zero checks.
Read two integers and print both integer division and floating-point division results.
Use modulo to check odd/even and to wrap a 24-hour clock value.
⚖️ 4.7 – Relational Operators
Relational (comparison) operators compare two values and always return 1 (true) or 0 (false). They are used in if, while, and for conditions.
Operator
Symbol
Meaning
Example (a=10, b=5)
Result
Equal to
==
Both values are equal
a == b
0 (false)
Not equal to
!=
Values are different
a != b
1 (true)
Greater than
>
Left is larger
a > b
1 (true)
Less than
<
Left is smaller
a < b
0 (false)
Greater or equal
>=
Left >= right
a >= 10
1 (true)
Less or equal
<=
Left <= right
b <= 5
1 (true)
#include<stdio.h>intmain() {
int a = 10, b = 5, c = 10;
printf("=== Relational Results ===\n");
printf("a == b : %d\n", a == b); // 0 — 10 ≠ 5printf("a == c : %d\n", a == c); // 1 — 10 = 10printf("a != b : %d\n", a != b); // 1printf("a > b : %d\n", a > b); // 1printf("a < b : %d\n", a < b); // 0printf("a >= c : %d\n", a >= c); // 1 — 10 >= 10printf("b <= 4 : %d\n", b <= 4); // 0 — 5 not <= 4/* ── In if conditions ── */if (a > b)
printf("a is greater than b\n");
/* ── Comparing floats: never use == directly! ── */double x = 0.1 + 0.2;
printf("0.1+0.2 == 0.3 → %d\n", x == 0.3); // 0! Floating-point imprecisiondouble eps = 1e-9;
printf("Near equal? → %d\n", (x - 0.3) < eps); // 1 — correct way/* ── Range check ── */int score = 75;
if (score >= 0 && score <= 100)
printf("Valid score\n");
/* ── Relational result used in arithmetic ── */int is_adult = (a >= 18); // stores 0 or 1printf("is_adult = %d\n", is_adult);
return0;
}
relational.c
📐 Relational Operator — Returns 0 or 1
a > b
→
a is larger → returns 1 (true)
a is NOT larger → returns 0 (false)
Result is always int: 1 = true, 0 = false. You can store it, print it, or use it in arithmetic.
📋 Program Output
► relational.c
=== Relational Results ===
a == b : 0
a == c : 1
a != b : 1
a > b : 1
a < b : 0
a >= c : 1
b <= 4 : 0
a is greater than b
0.1+0.2 == 0.3 → 0
Near equal? → 1
Valid score
is_adult = 0
🔍 Result Analysis:
a == b → 0: 10 ≠ 5, false. a == c → 1: 10 = 10, true.
a >= c → 1: equality also satisfies ≥. b <= 4 → 0: 5 ≤ 4 is false.
0.1+0.2 == 0.3 → 0: binary float cannot represent 0.1/0.2 exactly; their sum is ~0.30000000000000004, not 0.3.
is_adult = 0: a=10, so 10 >= 18 is false (0). The comparison result is stored as an integer.
Never compare floats with ==. Due to IEEE 754 floating-point representation, 0.1 + 0.2 is not exactly 0.3. Always check if |a - b| < epsilon instead.
📝 Topic Assignments — 4.7 Relational Operators
Compare two numbers using all six relational operators and print 0/1 results.
Build a range validator that checks if marks are between 0 and 100.
Compare floating-point values safely using epsilon logic.
🔢 4.8 – Increment & Decrement Operators
The ++ and -- operators increase or decrease a variable by exactly 1. They have two forms: prefix (change first, then use) and postfix (use first, then change).
PREFIX: ++x / --x
Increment/decrement FIRST, then evaluate expression
int x = 5;
int y = ++x; // x=6 first, y=6printf("%d %d", x, y); // 6 6
POSTFIX: x++ / x--
Evaluate expression FIRST, then increment/decrement
int x = 5;
int y = x++; // y=5 first, x=6printf("%d %d", x, y); // 6 5
#include<stdio.h>intmain() {
int a, b, x;
/* ── Post-increment: use value, THEN increment ── */
x = 5;
a = x++; // a gets OLD value (5), then x becomes 6printf("Post x++: a=%d, x=%d\n", a, x); // a=5, x=6/* ── Pre-increment: increment FIRST, then use ── */
x = 5;
a = ++x; // x becomes 6 FIRST, then a gets 6printf("Pre ++x: a=%d, x=%d\n", a, x); // a=6, x=6/* ── Post-decrement ── */
x = 10;
b = x--; // b gets 10, then x becomes 9printf("Post x--: b=%d, x=%d\n", b, x); // b=10, x=9/* ── Pre-decrement ── */
x = 10;
b = --x; // x becomes 9 FIRST, then b gets 9printf("Pre --x: b=%d, x=%d\n", b, x); // b=9, x=9/* ── Typical use in loops ── */printf("\nCounting up: ");
for (int i = 1; i <= 5; i++) // i++ is standard for-loop idiomprintf("%d ", i); // 1 2 3 4 5printf("\nCounting down: ");
for (int i = 5; i >= 1; i--) // i-- for reverse traversalprintf("%d ", i); // 5 4 3 2 1/* ── Difference in expression context ── */printf("\n\nExpression demo:\n");
int p = 3, q = 3;
printf("p++ * 2 = %d (p=%d)\n", p++ * 2, p); // 6, p=4printf("++q * 2 = %d (q=%d)\n", ++q * 2, q); // 8, q=4return0;
}
inc_dec.c
📐 Step-by-Step: Postfix vs Prefix in an Expression
Post x++: a=5, x=6 — a got the old value 5; x incremented after.
Pre ++x: a=6, x=6 — x incremented to 6 first, then a received 6.
p++ * 2 = 6: p=3 used in expression first (3×2=6), THEN p becomes 4.
++q * 2 = 8: q=3 incremented to 4 FIRST, then 4×2=8. Both p and q end as 4 but expressions differ.
Best practice: In standalone statements (i++; or ++i; alone), prefix and postfix produce identical results. Prefer ++i alone (prefix) when the return value is not used — it avoids storing a temporary copy (minor efficiency gain, especially for complex types).
📝 Topic Assignments — 4.8 Increment & Decrement
Write a demo that shows difference between x++ and ++x inside expressions.
Print numbers 1 to 20 using i++ and 20 to 1 using i--.
Trace values of variables in prefix/postfix combinations and explain each line.
❓ 4.9 – Conditional (Ternary) Operator ?:
The ternary operator is C's only three-operand operator. It is a compact form of if-else that produces a value. Syntax: condition ? expr_if_true : expr_if_false
result = condition ? value_if_true : value_if_false;
▲ Evaluated first. Non-zero = true.
▲ Used when condition is true
▲ Used when condition is false
#include<stdio.h>intmain() {
int a = 10, b = 20;
/* ── Basic ternary ── */int max = (a > b) ? a : b;
printf("max = %d\n", max); // 20/* ── Ternary in printf ── */printf("%d is %s\n", a, (a % 2 == 0) ? "even" : "odd"); // even/* ── Nested ternary (use sparingly, hard to read) ── */int marks = 72;
char *grade = (marks >= 90) ? "A" :
(marks >= 75) ? "B" :
(marks >= 60) ? "C" : "F";
printf("Grade: %s\n", grade); // C/* ── Ternary to select format string ── */int items = 1;
printf("You have %d %s\n", items, (items == 1) ? "item" : "items");
// "You have 1 item"/* ── Ternary to compute absolute value ── */int x = -7;
int abs_x = (x < 0) ? -x : x;
printf("|%d| = %d\n", x, abs_x); // |-7| = 7/* ── Ternary as l-value (assign to) ── */int y, z;
int use_y = 1;
// Pick which variable to assign to:
*( use_y ? &y : &z ) = 42;
printf("y=%d z=%d\n", y, z); // y=42 z=garbagereturn0;
}
ternary.c
📋 Program Output
► ternary.c
max = 20
10 is even
Grade: C
You have 1 item
|-7| = 7
y=42 z=0
🔍 Result Analysis:
max = 20: a > b → 10 > 20 is false → takes the b (20) branch.
10 is even: 10 % 2 == 0 is true → selects the string "even".
Grade: C: marks=72, not ≥90, not ≥75, but ≥60 → picks "C" from the nested chain.
You have 1 item: items==1 selects the singular form. Ternary is ideal for pluralisation.
|-7| = 7: x=-7 < 0 is true → selects -x = 7.
When to use ternary? Use it for simple one-line value selections. Avoid deeply nested ternaries — they become unreadable. For complex logic, use if-else instead.
📝 Topic Assignments — 4.9 Ternary Operator
Find maximum of two and three numbers using ternary operators.
Print whether a number is even/odd using only one ternary expression.
Create a grade classifier with nested ternary and compare with if-else readability.
🔗 4.10 – Logical Operators
Logical operators combine boolean (true/false) expressions. In C, any non-zero value is true; zero is false. All logical operators return 1 (true) or 0 (false).
Operator
Symbol
Name
Returns 1 (true) when
Logical AND
&&
AND
Both operands are true (non-zero)
Logical OR
||
OR
At least one operand is true
Logical NOT
!
NOT
Operand is false (zero)
Truth Tables
A
B
A && B
0
0
0
0
1
0
1
0
0
1
1
1
A
B
A || B
0
0
0
0
1
1
1
0
1
1
1
1
A
!A
0 (false)
1
non-zero
0
#include<stdio.h>intmain() {
int a = 5, b = 10, c = 0;
/* ── AND: both must be non-zero ── */printf("a&&b = %d\n", a && b); // 1 (5 and 10 both non-zero)printf("a&&c = %d\n", a && c); // 0 (c is zero)/* ── OR: at least one must be non-zero ── */printf("a||c = %d\n", a || c); // 1 (a is non-zero)printf("c||c = %d\n", c || c); // 0 (both zero)/* ── NOT: flips truth value ── */printf("!a = %d\n", !a); // 0 (5 is true, NOT → false)printf("!c = %d\n", !c); // 1 (0 is false, NOT → true)printf("!!a = %d\n", !!a); // 1 (normalises to 0 or 1)/* ── Compound conditions ── */int age = 22;
int has_id = 1;
if (age >= 18 && has_id)
printf("Access granted\n");
int is_weekend = 0, is_holiday = 1;
if (is_weekend || is_holiday)
printf("Day off!\n");
/* ── Short-circuit evaluation ── */// In &&: if left side is false, RIGHT IS NOT EVALUATEDint *ptr = NULL;
if (ptr != NULL && *ptr > 0) // Safe: *ptr not reached when ptr==NULLprintf("positive\n");
// In ||: if left side is true, right is NOT evaluatedint x = 5;
if (x > 0 || (printf("not printed\n"), 1))
printf("short-circuit OR\n"); // printf NOT called!return0;
}
logical.c
📐 Short-Circuit Evaluation Diagram
A && B (AND)
① Evaluate A
If A = false (0) → stop, return 0 (B never evaluated)
② If A = true → evaluate B
Return B's truth value
A || B (OR)
① Evaluate A
If A = true (non-zero) → stop, return 1 (B never evaluated)
a&&b = 1: a=5 (true) AND b=10 (true) → both non-zero → 1.
a&&c = 0: a=true but c=0 (false) → AND fails → 0.
a||c = 1: a=5 is truthy → short-circuits immediately, c never checked.
!a = 0: a=5 is truthy → NOT flips to false (0). !!a = 1: double NOT normalises non-zero to exactly 1.
short-circuit OR printed; but "not printed" text never appeared because x > 0 was already true so the right side of || was skipped.
Short-circuit evaluation is a critical feature: && stops at the first false operand; || stops at the first true operand. Use this to safely guard pointer dereferences: if (ptr != NULL && *ptr > 0)
📝 Topic Assignments — 4.10 Logical Operators
Build an eligibility checker (age and ID) using &&, ||, and !.
Print full truth tables for logical AND, OR, and NOT.
Demonstrate short-circuit behavior with side effects in conditions.
⚡ 4.11 – Bitwise Operators
Bitwise operators work directly on the binary representation of integers — one bit at a time. They are extremely fast and used in flags, graphics, encryption, and systems programming.
Operator
Symbol
Name
Operation
Bitwise AND
&
AND
1 if BOTH bits are 1
Bitwise OR
|
OR
1 if EITHER bit is 1
Bitwise XOR
^
XOR
1 if bits are DIFFERENT
Bitwise NOT
~
Complement
Flips all bits (0→1, 1→0)
Left Shift
<<
SHL
Shift bits left, fill with 0 (×2 per shift)
Right Shift
>>
SHR
Shift bits right (÷2 per shift for positive)
a = 12 (0000 1100) | b = 10 (0000 1010)
a & b
0000 1000
= 8
a | b
0000 1110
= 14
a ^ b
0000 0110
= 6
~a
1111 0011
= -13
a << 1
0001 1000
= 24
a >> 1
0000 0110
= 6
#include<stdio.h>// Helper: print binary representation of an intvoidprintBin(int n) {
for (int i = 7; i >= 0; i--)
printf("%d", (n >> i) & 1);
printf(" (%d)\n", n);
}
intmain() {
int a = 12, b = 10; // 0000 1100, 0000 1010printf("a = "); printBin(a); // 00001100 (12)printf("b = "); printBin(b); // 00001010 (10)printf("a&b = "); printBin(a&b); // 00001000 (8)printf("a|b = "); printBin(a|b); // 00001110 (14)printf("a^b = "); printBin(a^b); // 00000110 (6)printf("~a = "); printBin(~a); // 11110011 (-13)printf("a<<1 = "); printBin(a<<1);// 00011000 (24) ×2printf("a>>1 = "); printBin(a>>1);// 00000110 (6) ÷2/* ══ PRACTICAL APPLICATIONS ══ */printf("\n--- Bit Tricks ---\n");
// 1. Check if number is odd (test bit 0)int n = 13;
printf("%d is %s\n", n, (n & 1) ? "odd" : "even"); // odd// 2. Set bit k: OR with mask (1 << k)int flags = 0;
flags |= (1 << 3); // set bit 3 → 0b00001000 = 8
flags |= (1 << 5); // set bit 5 → 0b00101000 = 40printf("After setting bits 3,5: %d\n", flags); // 40// 3. Clear bit k: AND with inverse mask
flags &= ~(1 << 3); // clear bit 3 → 0b00100000 = 32printf("After clearing bit 3: %d\n", flags); // 32// 4. Toggle bit k: XOR with mask
flags ^= (1 << 5); // toggle bit 5 → 0printf("After toggling bit 5: %d\n", flags); // 0// 5. Check if bit k is set: AND with mask
flags = 0b10110110;
if (flags & (1 << 4))
printf("Bit 4 is SET\n");
// 6. Multiply/divide powers of 2 using shiftsprintf("5 * 8 = %d\n", 5 << 3); // 40 (5 × 2³)printf("64 / 4 = %d\n", 64 >> 2); // 16 (64 ÷ 4)// 7. Swap two numbers without temp variable (XOR swap)int x = 5, y = 9;
x ^= y; y ^= x; x ^= y; // classic XOR swapprintf("x=%d y=%d\n", x, y); // x=9, y=5return0;
}
bitwise.c
Common Bitwise Pattern
Code
Purpose
Set bit k
n |= (1 << k)
Force bit k to 1
Clear bit k
n &= ~(1 << k)
Force bit k to 0
Toggle bit k
n ^= (1 << k)
Flip bit k
Test bit k
(n >> k) & 1
Isolate bit k (0 or 1)
Check odd/even
n & 1
1 = odd, 0 = even
Multiply by 2ⁿ
n << k
n × 2^k
Divide by 2ⁿ
n >> k
n ÷ 2^k (positive only)
Get low byte
n & 0xFF
Mask upper bytes
📋 Program Output
► bitwise.c
a = 00001100 (12)
b = 00001010 (10)
a&b = 00001000 (8)
a|b = 00001110 (14)
a^b = 00000110 (6)
~a = 11110011 (-13)
a<<1 = 00011000 (24)
a>>1 = 00000110 (6)
--- Bit Tricks ---
13 is odd
After setting bits 3,5: 40
After clearing bit 3: 32
After toggling bit 5: 0
Bit 4 is SET
5 * 8 = 40
64 / 4 = 16
x=9 y=5
🔍 Result Analysis:
a & b = 8: 1100 AND 1010 → only bit 3 is 1 in both → 00001000 = 8.
a | b = 14: bits that are 1 in either → 00001110 = 14.
a ^ b = 6: bits that differ (positions 1,2) → 00000110 = 6.
~a = -13: all bits of 12 (00001100) flipped → 11110011 = -13 in two's complement.
a << 1 = 24: left shift by 1 = ×2 → 12×2 = 24. a >> 1 = 6: right shift = ÷2.
/* Precedence examples — predict the output! */int a = 2, b = 3, c = 4;
printf("%d\n", a + b * c); // 14 (* before +)printf("%d\n", (a + b) * c); // 20 (parens first)printf("%d\n", a < b && b < c); // 1 (< before &&)printf("%d\n", !a == 0); // 1 (! first, then ==)printf("%d\n", a | b & c); // 2 (& before |): a|(b&c) = 2|(3&4) = 2|0 = 2printf("%d\n", a + b > c); // 1 (5 > 4 = true)// Rule: When unsure, ADD PARENTHESES — they cost nothing!precedence_demo.c
Golden Rule: Operator precedence is a source of many bugs. When mixing operator types (e.g. bitwise + logical), always add parentheses: write (a & mask) != 0 not a & mask != 0 (which is a & (mask != 0) — probably not what you want!).
Why & in scanf?scanf needs to write into your variable. Without &, you'd pass the value; with &, you pass the address so scanf can store data there.
📝 Topic Assignments — 4.13 scanf Input
Read name, age, height, and grade from user and print a formatted profile card.
Build a small input form that validates age range and marks range using conditions.
Demonstrate safe string input using width-limited specifiers in scanf.
📝 Section 4.5 Assignments — Assignment Operators
Compound Chain: Declare int x = 100. Apply in sequence: x += 25, x -= 15, x *= 2, x /= 3, x %= 7. Print x after every step and verify the final value manually.
All Assignments Table: Declare int a = 60. Print the result of every compound assignment operator (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=) applied to a with operand 3. Reset a = 60 before each.
Chained Assignment: Declare three variables p, q, r. Assign all three to 999 using a single chained assignment statement p = q = r = 999. Print all three and explain the right-to-left evaluation order.
Accumulator Loop: Use += inside a for loop to compute the sum of numbers 1 to 50. Do NOT use a separate addition expression — only sum += i. Print the final sum (expected: 1275).
Running Product: Read 5 integers from the user. Use *= to compute their product iteratively. Print the product after each multiplication step showing how it grows.
Temperature Tracker: Read 7 daily temperatures. Use += to keep a running total, then use /= to compute and print the weekly average. Use only compound assignment — no standalone total = total + t.
Score Adjustment: A student starts with score = 50. Apply these rules using compound assignment: add 10 for attendance, subtract 5 for late submission, multiply by 1 (no bonus), divide by 1 (no penalty), add remainder of score÷3 as a bonus. Print final score.
= vs == Bug Hunt: Write a program where you intentionally write if (x = 10) and observe that it always evaluates to true. Then fix it to if (x == 10). Print which version correctly checks equality vs which version silently assigns. Add comments explaining the bug.
Bitwise Flag Manager: Declare int perm = 0 representing 8 permission bits. Use |= to set bits 0, 2, and 5. Use &= ~(1<<2) to clear bit 2. Use ^= to toggle bit 5. Print the final integer value and its binary equivalent (manually or with a loop).
Shift Assignment: Start with int n = 1. Use <<= 1 in a loop 8 times, printing n after each shift. Observe that you're computing powers of 2 (1, 2, 4, 8, 16, 32, 64, 128, 256). Then use >>= 1 8 times to reverse.
📝 Section 4.6 Assignments — Arithmetic Operators
Basic Calculator: Read two integers a and b from the user. Print the result of all five arithmetic operations: a+b, a-b, a*b, a/b (integer), a%b. Handle b=0 for division and modulo.
Integer vs Float Division: Read two integers. Print both integer division (a/b) and real division ((double)a/b) side by side. Add a note in the output explaining why they differ. Try inputs like 7/2, 10/3, 15/4.
Digit Extractor: Read a 3-digit integer (100–999). Using only / and %, extract and print the hundreds digit, tens digit, and units digit separately. Example: 357 → hundreds=3, tens=5, units=7.
Time Converter: Read a total number of seconds (e.g., 3725). Using / and %, convert it to hours, minutes, and remaining seconds. Print as H:MM:SS. Example: 3725 → 1:02:05.
Unary Minus Practice: Read any integer. Print its negation using unary minus. Then compute -(-n) and show it equals the original. Also compute -(n*2) and compare with (-n)*2. Show they are equal.
Circle Geometry: Read a radius as a double. Using arithmetic operators (no math library functions), compute:
(a) Circumference = 2 × 3.14159 × r
(b) Area = 3.14159 × r × r
Print both to 4 decimal places.
Overflow Detector: Declare int x = 2147483647 (INT_MAX). Print x, then print x + 1. Observe integer overflow. Repeat with short (MAX = 32767). Explain in comments what overflow means and why it wraps around.
Modulo Clock: Read a starting hour (0–23) and a number of hours to add (any positive integer). Use %24 to compute the final hour on a 24-hour clock. Examples: start=22, add=5 → 3; start=0, add=36 → 12.
Fibonacci Step: Start with int a=0, b=1. Using only assignment and arithmetic operators (no loops), manually compute and print the first 10 Fibonacci numbers by repeatedly doing c = a+b; a = b; b = c;.
BMI Calculator: Read weight in kg (double) and height in metres (double). Calculate BMI = weight / (height × height) using arithmetic operators. Print the BMI to 2 decimal places and classify: "Underweight" (<18.5), "Normal" (18.5–24.9), "Overweight" (25–29.9), "Obese" (≥30).
📝 Section 4.7 Assignments — Relational Operators
Comparison Table: Read two integers a and b. Print the result of all six relational operators (==, !=, >, <, >=, <=) as 0 or 1 with labels. Example output: a==b : 0, a!=b : 1, etc.
Grade Classifier: Read a marks value (0–100). Using relational operators in if-else if, print the grade: A (≥90), B (≥80), C (≥70), D (≥60), F (<60). Also print "Invalid" if marks <0 or >100.
Largest of Three: Read three integers. Using only relational operators and if-else (no ternary), find and print the largest. Then extend to also print the smallest.
Year Validator: Read a year. Use relational operators to check:
(a) Is it a future year (after 2025)?
(b) Is it in the 21st century (2001–2100)?
(c) Is it a round century year (divisible by 100)?
Print YES/NO for each check.
Float Comparison Fix: Compute double x = 0.1 + 0.2. Compare it with 0.3 using == (will fail). Then compare using fabs(x - 0.3) < 1e-9 (will pass). Print both results and explain floating-point imprecision in comments.
Number Sign Checker: Read an integer. Using relational operators (>, <, ==), determine if it is positive, negative, or zero. Print a clear message. Then also check if it is divisible by both 2 and 5.
Triangle Validator: Read three sides of a triangle. Using relational operators, check if they form a valid triangle (each side must be less than the sum of the other two). Print "Valid Triangle" or "Invalid Triangle".
Relational in Arithmetic: C relational expressions return 0 or 1 as integers. Exploit this: read n and compute count = (n>0) + (n%2==0) + (n<100). Print count (0-3) and explain what each term counts. Example: n=50 → count=3.
Leap Year Checker: Read a year. Using relational and modulo operators, determine if it is a leap year (divisible by 4, except centuries unless divisible by 400). Print "Leap Year" or "Not a Leap Year" with the condition that matched.
Between Checker: Read three integers: low, value, high. Using relational operators, check and print: is value strictly between low and high? Is it within [low, high] inclusive? Is it outside the range? Test edge cases where value equals low or high.
Pre vs Post Trace: Write a program with int x = 10. Evaluate and print these one per line: x++, x, ++x, x, x--, x, --x, x. Before running, manually predict every output value and write your predictions as comments. Verify against actual output.
Expression Tracing: Given int a=3, b=5: compute and print a++ + b, current values of a and b, then ++a + b--, then final values. Explain each step in detail with comments.
Countdown Timer: Read a positive integer n. Use a while loop with n-- (post-decrement) in the condition to print: "T-5", "T-4", ..., "T-0", "Launch!". Observe that the loop uses the value before decrementing for the print.
Array Traversal: Declare int arr[] = {10,20,30,40,50}. Use a for loop with i++ to print elements forward. Then use a separate loop from index 4 to 0 using i-- to print them in reverse. Print both sequences.
Multiplication Table: Read a number n. Use a for loop with i++ (i from 1 to 10) to print the multiplication table of n. Format: n × i = result per line.
Sum with Pre-increment: Use ++i inside a for loop (i from 0, condition i < 10, no increment in header) to sum 1–10. Compare with version using i++. Show both sums are equal (1275 for 1–50, 55 for 1–10) and explain why.
Digit Counter: Read a non-negative integer. Use a while loop with n /= 10 and a separate counter incremented with count++ to count the number of digits. Handle the special case n=0. Print the digit count.
Pointer Increment: Declare int arr[5] = {5,10,15,20,25} and a pointer int *p = arr. Use p++ in a loop to traverse and print each element using *p. Explain how pointer increment moves by sizeof(int) bytes, not by 1.
FizzBuzz with ++: Print numbers 1–30 using a for loop with i++. For multiples of 3 print "Fizz", multiples of 5 print "Buzz", multiples of both print "FizzBuzz", otherwise print the number. Use % and relational operators for checks.
Side-Effect Warning: Write int i=5; printf("%d %d", i++, i++); — predict the output, run it, then explain why the output is compiler-dependent (undefined evaluation order between function arguments). Rewrite safely using separate statements to get deterministic output.
Max of Two: Read two integers. Use a single ternary expression to find and print the larger value. Then extend to find the maximum of three numbers using two nested ternaries.
Absolute Value: Read any integer. Use a ternary expression to compute its absolute value without using <math.h>: abs = (n < 0) ? -n : n. Print the result.
Even/Odd Formatter: Read 10 integers in a loop. For each, use a ternary to print "n is even" or "n is odd" on one line. Write the entire output statement as a single printf using the ternary inside the argument.
Grade String: Read a numeric score (0–100). Use nested ternary operators to assign a grade string: "A" (≥90), "B" (≥80), "C" (≥70), "D" (≥60), "F" (<60). Print "Score 75 → Grade C".
Plural Selector: Read a count of items. Use ternary to print the grammatically correct noun form: "1 apple" vs "2 apples", "1 ox" vs "2 oxen", "1 person" vs "2 people". The nouns should be selected by ternary, not by if-else.
Min of Array: Declare int arr[5] = {34, 12, 78, 5, 56}. Use a for loop and at each step update min = (arr[i] < min) ? arr[i] : min. Print the minimum value (expected: 5).
Ternary Assignment: Demonstrate that ternary can be an l-value argument using pointer dereferencing: read two integers a and b and a flag. If flag=1, assign 99 to a; if flag=0, assign 99 to b — using the expression *(flag ? &a : &b) = 99. Print both a and b after.
Ternary vs if-else: Write the same logic twice — once using if-else and once using ternary — to find the sign of a number (+1, 0, or -1). Confirm both produce identical output. Add a comment on when ternary is preferred over if-else.
Safe Division: Read two integers a and b. Use a ternary to compute a/b only if b != 0, otherwise print "Division by zero!". Write the entire logic as a single printf with ternary inside.
Triangle Type: Read three sides. Use nested ternary to classify the triangle as "Equilateral" (all sides equal), "Isosceles" (two sides equal), or "Scalene" (all different). Print the classification. No if-else allowed — ternary only.
📝 Section 4.10 Assignments — Logical Operators
Truth Table Printer: Write a program that prints the full truth table for &&, ||, and ! for all combinations of A=0/1 and B=0/1. Format as a table with headers. There should be 4 rows and 5 columns (A, B, A&&B, A||B, !A).
Range Validator: Read an integer. Use && to check if it falls in the range [1, 100] inclusive. Use || to check if it is outside (i.e., <1 or >100). Print appropriate messages for each check.
Login System: Define username="admin" and password=1234. Read both from user. Use && for "both correct → Access Granted", || for "at least one wrong → Access Denied", and ! to negate correct flags. Print which condition triggered.
Short-Circuit Proof: Write a function int sideEffect() that prints "CALLED" and returns 1. In main: evaluate 0 && sideEffect() — show "CALLED" is NOT printed. Evaluate 1 || sideEffect() — show "CALLED" is NOT printed. Explain short-circuit evaluation.
NULL Pointer Guard: Declare int *p = NULL. Use p != NULL && *p > 0 to safely check without crashing. Then assign p to a valid integer variable with value 42 and repeat the check. Show both results and explain why the first doesn't segfault.
Voting Eligibility: Read age and citizenship status (1=citizen, 0=not). Use logical operators to determine: Can vote? (age≥18 AND citizen), Cannot vote (age<18 OR not citizen). Print detailed eligibility message with reason.
Leap Year with Logic: Read a year. Use && and || to correctly implement the leap year rule: (year%4==0 && year%100!=0) || (year%400==0). Print "Leap" or "Not Leap". Test with 2000, 1900, 2024, and 2100.
NOT Operator Practice: Declare int found = 0. Use !found to print "Not found yet". Then set found = 1. Use !found again to confirm it now prints nothing. Use !!found to normalize any non-zero value to exactly 1.
Multi-Condition Filter: Read 10 integers in a loop. Print only those that satisfy ALL three conditions: greater than 10, less than 100, AND divisible by 3. Use a single if with && to combine all conditions.
De Morgan's Law: Prove De Morgan's laws in code. For any two integer inputs A and B, verify: !(A && B) == (!A || !B) and !(A || B) == (!A && !B). Test with all four combinations (0,0), (0,1), (1,0), (1,1) and print PASS/FAIL for each.
📝 Section 4.11 Assignments — Bitwise Operators
Binary Printer: Write a function void printBinary(int n) that prints the 8 least-significant bits of n using a loop and (n >> k) & 1. In main, call it for: 0, 1, 127, 128, 255, -1, 12, 85.
Bitwise Operations Table: Set int a=60, b=13. Print and explain (in comments) the result of: a&b, a|b, a^b, ~a, a<<2, a>>2. For each, also print the binary representation using your printBinary function.
Bit Manipulation Suite: Start with int n = 0. Perform these operations in order using bitwise compound assignment: set bits 1, 3, 5, 7; clear bit 3; toggle bit 5; check if bit 7 is set. Print n after each step and the binary using printBinary.
Permission System: Simulate Unix file permissions. Define: READ=4, WRITE=2, EXEC=1. Read a permission byte (0–7). Print "r", "w", "x" or "-" for each bit. Read a second number for owner/group/other (0–777 octal-like input 0–7 three times) and print rwxr-xr-- style output.
Odd/Even Fast Check: Read 10 integers. For each, use n & 1 to determine odd/even (faster than %2). Print the result next to each number. Also verify that (n & 1) == (n % 2) for all cases (note: careful with negatives).
Power of Two Checker: A number is a power of 2 if and only if n > 0 && (n & (n-1)) == 0. Read 10 integers and for each print whether it is a power of 2. Test with 1, 2, 3, 4, 7, 8, 16, 32, 63, 64.
Bit Counting (Hamming Weight): Count the number of 1-bits in an integer (its "popcount"). Use a loop: while n != 0, do count += n & 1; n >>= 1. Print the result for values 0, 7, 15, 85 (0b01010101 = 4 bits set), 255.
Swap Without Temp: Use the XOR swap trick — a ^= b; b ^= a; a ^= b; — to swap two integers without a temporary variable. Read two integers, swap them, print before and after. Then explain in comments why this works using truth table logic.
Color Channel Extractor: An RGB color is stored as a 32-bit integer: 0x00RRGGBB. Given color 0x00FF8040, extract the Red, Green, and Blue channels using bitwise AND and right-shift. Red = (color >> 16) & 0xFF, Green = (color >> 8) & 0xFF, Blue = color & 0xFF. Print each channel value (expected: R=255, G=128, B=64).
Multiply/Divide by Powers of 2: Read an integer n. Using only shift operators, compute and print: n×2, n×4, n×8, n÷2, n÷4. Then compare results with normal arithmetic (*2, *4, etc.) to verify correctness. Discuss when shifts are preferred over multiplication.
Manual Evaluation: Without running the code, compute the final value of x in each line, then verify by running:
(a) int x = 2 + 3 * 4; (b) int x = 10 - 4 / 2 + 1; (c) int x = 15 % 4 * 2; (d) int x = 2 << 1 + 1; (e) int x = 8 / 2 * 4;
Parentheses Matter: For each pair below, predict both values before running. Then print both:
(a) 3 + 4 * 2 vs (3 + 4) * 2 (b) 10 / 2 + 3 vs 10 / (2 + 3) (c) !0 + 1 vs !(0 + 1) (d) 2 & 3 | 4 vs 2 & (3 | 4)
Relational + Logical Precedence: Predict and verify:
(a) 3 > 2 && 5 < 10 (both true → 1)
(b) 3 > 2 + 1 (2+1 first → 3>3 → 0)
(c) 1 + 2 > 2 + 1 (both sides computed first)
(d) !1 + !0 (! before + → 0 + 1 = 1)
Print each result with its computed value as a comment.
Assignment Precedence: Explain and demonstrate:
(a) Right-to-left: int a=1,b=2,c=3; a = b = c = 10; — print all three
(b) int x = 5; x += 2 * 3; — show that * happens before +=
(c) int y = (3 + 4) * (2 - 1); — use parens to override default precedence
Bitwise Precedence Trap: The classic bug: if (flags & MASK != 0) is actually parsed as flags & (MASK != 0) because != has higher precedence than &! Write a program demonstrating this bug, then fix it with if ((flags & MASK) != 0). Print both results.
Ternary Associativity: Ternary is right-associative. Evaluate:
int x = 1 ? 2 ? 3 : 4 : 5;
First predict the value manually (answer: 3). Then run and verify. Explain how right-to-left associativity means it is parsed as 1 ? (2 ? 3 : 4) : 5.
Unary Minus Priority: Predict and verify:
(a) -2 + 3 → unary minus applied first → (-2)+3 = 1
(b) -2 * -3 → 6 (two unary minuses)
(c) -(2 + 3) → -5 (parens override)
(d) !0 * 5 → 5 (!0=1, then 1*5)
Print all results.
Complex Expression Dissection: Fully parenthesize (add explicit parentheses to show evaluation order) each expression, then run to verify:
(a) a + b > c && d - e < f where a=3,b=4,c=6,d=10,e=3,f=8
(b) x = y = 5 + 3 * 2 where x and y initially 0
Print the fully parenthesized form as a string comment alongside the result.
Short-Circuit and Precedence: Demonstrate that in a || b && c, the && binds first (higher precedence), so it's a || (b && c) — NOT (a || b) && c. Write a program where the two interpretations give different results and print both to prove the point.
Precedence Table Quiz: Write a program that tests 5 more complex expressions.for each: print what you predicted, the actual computed result, and PASS/FAIL:
(a) 2 | 3 ^ 1 & 7 (expected: 3)
(b) 1 << 2 + 1 (expected: 8)
(c) ~0 & 0xFF (expected: 255)
(d) 4 / 2 == 2 + 0 (expected: 1)
(e) 3 != 2 + 1 (expected: 0)
Unit 5 – Control Statements
Control statements determine the flow of your program — which code runs, how many times, and under what conditions. This unit covers all decision-making and looping constructs with flowcharts for every construct.
🧠 5.0 – if Family Fundamentals (Simple if, if-else, Nested if-else)
Start with these three patterns. They are the base of all decision making in C.
1) Simple if
Explanation: Executes a block only when the condition is true. If false, program skips the block and continues.
// Syntaxif (condition) {
// runs only when condition is true
}
// Example: check adult ageint age = 20;
if (age >= 18) {
printf("Eligible to vote\n");
}
2)if-else
Explanation: Exactly one of two blocks executes. Use this when you have two mutually exclusive outcomes.
// Example: even/oddint n = 17;
if (n % 2 == 0) {
printf("Even\n");
} else {
printf("Odd\n");
}
3) Nested if-else
Explanation: An if-else inside another if block. Use it when the second decision depends on the first decision.
// Syntaxif (condition1) {
if (condition2) {
// block A
} else {
// block B
}
} else {
// block C
}
// Example: pass + distinctionint marks = 82;
if (marks >= 50) {
if (marks >= 75) {
printf("Pass with Distinction\n");
} else {
printf("Pass\n");
}
} else {
printf("Fail\n");
}
Important: In C, non-zero means true and zero means false. Always use braces {} even for one-line bodies to avoid bugs and improve readability.
🔀 5.1 – if / else if / else
📋 Syntax
if (condition1) {
// runs when condition1 is true
} else if (condition2) {
// runs when condition2 is true (condition1 false)
} else {
// runs when ALL above conditions are false
}
// Ternary shorthand:
result = (condition) ? value_if_true : value_if_false;
Rules: Only one block executes. The else if and else clauses are optional. Conditions must evaluate to 0 (false) or non-zero (true).
#include<stdio.h>intmain() {
int marks;
printf("Enter marks (0-100): ");
scanf("%d", &marks);
if (marks >= 90) {
printf("Grade: A (Excellent)\n");
} else if (marks >= 75) {
printf("Grade: B (Good)\n");
} else if (marks >= 60) {
printf("Grade: C (Average)\n");
} else if (marks >= 50) {
printf("Grade: D (Pass)\n");
} else {
printf("Grade: F (Fail)\n");
}
return0;
}
grades.c
⚡ Ternary Operator (shorthand if-else)
// Syntax: condition ? value_if_true : value_if_falseint max = (a > b) ? a : b; // larger of a, bchar *type = (n % 2 == 0) ? "even" : "odd";
printf("%d is %s\n", n, (n >= 0) ? "non-negative" : "negative");
🔧 5.2 – switch Statement
📋 Syntax
switch (expression) { // expression must be int or charcase value1:
// code for case 1break; // EXIT switch - required to stop fall-throughcase value2:
// code for case 2break;
case value3: // multiple cases, same action (fall-through)case value4:
// runs for value3 OR value4break;
default: // optional: runs if NO case matched// fallback code
}
Key rules: expression must produce an integer (or char). Case values must be compile-time constants. break prevents fall-through. switch cannot test ranges — use if-else for that.
Don't forget break! Without it, execution "falls through" to the next case. This is sometimes intentional (case 6 & 7 above) but usually a bug.
⚠️ 5.6 – The Dangling Else Problem
When if statements are nested, an else always belongs to the nearest preceding unmatched if. This can produce unexpected behaviour when indentation doesn't match the compiler's parsing rules.
⚠️ Misleading indentation
int x = 10, y = 20;
if (x > 5)
if (y > 15)
printf("A\n");
elseprintf("B\n"); // which if does this belong to?// Answer: the INNER if (y > 15)
// Not the outer if (x > 5)!
// So "B" never prints when x <= 5
✅ Fixed with braces
int x = 10, y = 20;
if (x > 5) {
if (y > 15)
printf("A\n");
} else {
printf("B\n"); // now unambiguously
} // belongs to outer if// Always use braces {} even for
// single-statement bodies to prevent
// dangling else bugs
Best practice: Always use curly braces {} around if/else bodies, even when the body is a single statement. This eliminates the dangling else trap and makes future code additions safer.
🎚️ 5.7 – Switch Fall-Through Behaviour
After a matching case label is entered, execution falls through into subsequent case blocks unless a break (or return) stops it. Fall-through is a deliberate C language feature — it can be used intentionally to share code across cases, but it is a common source of bugs when left accidental.
⚠️ Accidental fall-through (bug)
int day = 2;
switch (day) {
case1: printf("Monday\n"); // no break!case2: printf("Tuesday\n"); // no break!case3: printf("Wednesday\n");
break;
}
// Output when day=2:
// Tuesday ← matched
// Wednesday ← fell through! (bug)
#include<stdio.h>/* Full switch example: menu-driven program */intmain() {
int choice;
printf("1=Add 2=Sub 3=Mul 4=Div 0=Quit\nChoice: ");
scanf("%d", &choice);
double a, b;
if (choice != 0) {
printf("Enter two numbers: ");
scanf("%lf %lf", &a, &b);
}
switch (choice) {
case1: printf("%.2f + %.2f = %.2f\n", a, b, a+b); break;
case2: printf("%.2f - %.2f = %.2f\n", a, b, a-b); break;
case3: printf("%.2f * %.2f = %.2f\n", a, b, a*b); break;
case4:
if (b == 0)
printf("Error: division by zero!\n");
elseprintf("%.2f / %.2f = %.2f\n", a, b, a/b);
break;
case0: printf("Goodbye!\n"); break;
default: printf("Invalid choice.\n");
}
return0;
}
calculator.c
Rules for switch: (1) The expression must evaluate to an integer type (int, char, enum). (2) case labels must be compile-time integer constants. (3) Always include default to handle unmatched values. (4) Add break after every case unless you intentionally want fall-through.
Unit 5 – Lab Exercises
Write a program to check if a number is positive, negative, or zero using if-else if-else.
Write a simple calculator using switch: read two numbers and an operator (+, -, *, /). Print the result. Handle division by zero.
Use nested if-else to check both sign (positive/negative) and parity (even/odd) of the input and print all four combinations (e.g., "Positive Even").
Write a vowel/consonant checker using switch with grouped cases (a, e, i, o, u → vowel; default → consonant). Handle uppercase and lowercase.
Demonstrate the dangling-else problem: write two versions of a nested if statement — one where the else attaches to the outer if and one where it attaches to the inner if — and print which branch executes for the same input.
Loop exercises (for, while, do-while) are in Unit 6.
Unit 6 – Loops in C
Loops eliminate repetitive code by executing a block repeatedly until a condition changes. C provides three loop constructs — for, while, and do-while — each optimised for different situations. This unit gives deep coverage of every loop type with formal syntax, annotated flowcharts, step-trace tables, and practical algorithms.
❓ 6.1 – Why Loops? The Repetition Problem
Without loops, repeating an action requires copy-pasted code that cannot scale:
/* WITHOUT a loop - print 1 to 5 */printf("%d\n", 1);
printf("%d\n", 2);
printf("%d\n", 3);
printf("%d\n", 4);
printf("%d\n", 5); /* what about 1 to 10 000? *//* WITH a for loop - scales to any N */for (int i = 1; i <= 5; i++) {
printf("%d\n", i);
}motivation.c
Loop type
Best used when…
Condition checked
Min executions
for
Number of iterations known in advance
Before each iteration
0
while
Loop count depends on a runtime condition
Before each iteration
0
do-while
Body must run at least once (menus, prompts)
After each iteration
1
Four parts of every loop: (1) Initialisation — set the starting state; (2) Condition — test before (or after) each iteration; (3) Body — the repeated work; (4) Update — change state to eventually make the condition false.
🔢 6.2 – for Loop — Deep Dive
📋 Syntax
for (initialisation; condition; update) {
body;
}
// Execution order every iteration:// (1) initialisation - runs ONCE only, before loop starts// (2) condition check - if FALSE exit loop immediately// (3) body executes// (4) update runs// (5) back to step 2// Common variants:for (int i = 0; i < n; i++) // count-up 0,1,...,n-1for (int i = n; i >= 1; i--) // count-down n,...,1for (int i = 0; i < n; i += 2) // step by 2for (int i=0, j=n-1; i<j; i++,j--) // two variablesfor (;;) // infinite (all parts omitted)
Flowchart
Step-trace: for(i=0; i<4; i++) printf(i)
Step
i before
i<4?
Action
i after
Init
0
✓ true
prints 0
1
2
1
✓ true
prints 1
2
3
2
✓ true
prints 2
3
4
3
✓ true
prints 3
4
5
4
✗ FALSE
exit loop
—
#include<stdio.h>intmain() {
/* Counter loop */for (int i = 1; i <= 5; i++)
printf("%d ", i); // 1 2 3 4 5printf("\n");
/* Accumulator - sum 1 to 100 */int sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
printf("Sum 1..100 = %d\n", sum); // 5050/* Factorial - product 1..n */long fact = 1;
for (int i = 2; i <= 10; i++)
fact *= i;
printf("10! = %ld\n", fact); // 3628800/* Two-variable loop - reverse an array */int a[] = {1,2,3,4,5};
for (int l=0, r=4; l < r; l++, r--) {
int t = a[l]; a[l] = a[r]; a[r] = t;
}
/* a is now {5,4,3,2,1} */return0;
}
for_deepdive.c
🔄 6.3 – while Loop — Deep Dive
📋 Syntax
while (condition) {
body;
// MUST update something to eventually make condition false
}
// Condition checked BEFORE every iteration.
// If condition is false from the start, body NEVER executes.
// Typical patterns:int i = 0;
while (i < n) { body; i++; } // equivalent to a for loopwhile (ch != 'q') { scanf("%c",&ch); } // until user quitswhile (!feof(fp)) { read_line(); } // until end of file
Step-trace: digit extraction from n=1234
Iteration
n
n>0?
digit = n%10
n after /=10
1
1234
true
4
123
2
123
true
3
12
3
12
true
2
1
4
1
true
1
0
5
0
FALSE
—
exit
#include<stdio.h>intmain() {
/* Sum of digits of 1234 */int n = 1234, sum = 0;
while (n > 0) {
sum += n % 10; // extract last digit
n /= 10; // remove last digit
}
printf("Digit sum = %d\n", sum); // 10/* Reverse a number */int num = 5678, rev = 0;
while (num > 0) {
rev = rev * 10 + num % 10;
num /= 10;
}
printf("Reversed: %d\n", rev); // 8765/* Count digits */int x = 98765, count = 0;
while (x != 0) { x /= 10; count++; }
printf("Digits: %d\n", count); // 5return0;
}
while_deepdive.c
🔁 6.4 – do-while Loop — Deep Dive
📋 Syntax
do {
body; // executed FIRST -- always runs at least once
} while (condition); // semicolon is required here!// Key distinction from while:int x = 100;
while (x < 5) { printf("while\n"); } // prints nothingdo { printf("do-while\n"); }
while (x < 5); // prints once!
#include<stdio.h>intmain() {
/* Menu loop - show once, repeat until quit */int choice;
do {
printf("\n=== MENU ===\n");
printf("1. Add\n2. Subtract\n0. Quit\n");
printf("Choice: ");
scanf("%d", &choice);
switch (choice) {
case1: printf("Add selected\n"); break;
case2: printf("Subtract selected\n"); break;
case0: printf("Goodbye!\n"); break;
default: printf("Invalid choice\n");
}
} while (choice != 0);
/* Validated input with do-while */int age;
do {
printf("Enter age (1-120): ");
scanf("%d", &age);
} while (age < 1 || age > 120);
printf("Valid age: %d\n", age);
return0;
}
do_while_deepdive.c
Rule of thumb: Use do-while when the body must execute at least once before the condition is tested — classic examples are menus, “try again” prompts, and reading the first element of a stream.
∞ 6.5 – Infinite Loops and How to Exit Them
📋 Three ways to write an infinite loop
for (;;) { body; } // most common style in systems codewhile (1) { body; } // 1 is always non-zero (true)do { body; } while(1); // less common// Exit with break inside a conditional:while (1) {
int input;
scanf("%d", &input);
if (input == 0) break; // controlled exitprintf("Got: %d\n", input);
}
#include<stdio.h>/* Simulated server event loop - runs "forever" */intmain() {
int req = 1;
for (;;) {
printf("Handling request #%d\n", req++);
/* In real server: accept socket, serve, repeat */if (req > 5) break; // simulate graceful shutdown
}
printf("Server shut down.\n");
return0;
}
// Ctrl+C kills any truly infinite loop in the terminalevent_loop.c
Avoiding accidental infinite loops: Always ensure the loop variable is updated inside the body, the condition can eventually become false, and break is reachable. Use gcc -Wall to catch suspicious loop patterns. Debug with printf or GDB if a program hangs.
🛑 6.6 – break Statement — Deep Dive
📋 Syntax & Behaviour
break; // exits the INNERMOST enclosing loop or switch// Works in: for, while, do-while, switch
// Does NOT break outer loops (only innermost)
// To break outer loop: use a flag variablefor (int i = 0; i < n; i++) {
if (arr[i] == target) {
printf("Found at %d\n", i);
break; // stop searching after first match
}
}
// Breaking outer loop using a flag:int done = 0;
for (int i = 0; i < ROWS && !done; i++)
for (int j = 0; j < COLS; j++)
if (grid[i][j] == target) { done = 1; break; }
#include<stdio.h>intmain() {
/* Linear search with break - stops at first match */int data[] = {15, 3, 42, 8, 27};
int target = 42, found = -1;
for (int i = 0; i < 5; i++) {
if (data[i] == target) { found = i; break; }
}
(found >= 0) ? printf("Found at %d\n", found)
: printf("Not found\n");
/* break exits ONLY the innermost loop */for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) break; // breaks inner, outer still runs!printf("(%d,%d) ", i, j);
}
}
printf("\n"); // prints (0,0) (1,0) (2,0)return0;
}
break_demo.c
⏩ 6.7 – continue Statement — Deep Dive
📋 Syntax & Behaviour
continue; // skips REST of current iteration// In a for loop: jumps to the UPDATE, then condition check// In a while loop: jumps directly to the condition check// WARNING: in while, if skip variable never changes -> infinite loop!for (int i=0; i<10; i++) {
if (i%2==0) continue; // skip even (i++ still runs)printf("%d ", i); // prints odd: 1 3 5 7 9
}
#include<stdio.h>intmain() {
/* Skip even numbers */printf("Odd 1-10: ");
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue;
printf("%d ", i); // 1 3 5 7 9
}
printf("\n");
/* Skip multiples of 3 */printf("No mult-of-3 (1-15): ");
for (int i = 1; i <= 15; i++) {
if (i % 3 == 0) continue;
printf("%d ", i); // 1 2 4 5 7 8 10 11 13 14
}
printf("\n");
/* Sum only positive inputs from user */int total = 0, n;
printf("Enter 5 numbers: ");
for (int i = 0; i < 5; i++) {
scanf("%d", &n);
if (n <= 0) continue; // skip negatives and zero
total += n;
}
printf("Sum of positives: %d\n", total);
return0;
}
continue_demo.c
🌀 6.8 – Nested Loops
📋 Syntax
for (int outer = 0; outer < ROWS; outer++) {
for (int inner = 0; inner < COLS; inner++) {
// inner loop body runs COLS times per outer iteration// total executions = ROWS x COLS (O(n^2) complexity)
}
}
// Each inner loop variable is independent of the outer one.
// Best practice: keep nesting depth <= 3 levels for readability.
#include<stdio.h>intmain() {
/* Pattern 1: Right-angle triangle */for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) printf("* ");
printf("\n");
} // * / * * / * * * / * * * * / * * * * */* Pattern 2: Number pyramid */for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) printf("%d ", j);
printf("\n");
} // 1 / 1 2 / 1 2 3 / 1 2 3 4 / 1 2 3 4 5/* Pattern 3: Multiplication table */printf("\nMultiplication table 1-5:\n");
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 5; j++)
printf("%4d", i*j);
printf("\n");
}
/* Pattern 4: Diamond of stars */int n = 5;
for (int i = 1; i <= n; i++) {
for (int sp = 0; sp < n-i; sp++) printf(" ");
for (int j = 0; j < 2*i-1; j++) printf("*");
printf("\n");
}
for (int i = n-1; i >= 1; i--) {
for (int sp = 0; sp < n-i; sp++) printf(" ");
for (int j = 0; j < 2*i-1; j++) printf("*");
printf("\n");
}
return0;
}
nested_patterns.c
🧮 6.9 – Common Loop Algorithms
#include<stdio.h>#include<math.h>// sqrt() - compile with -lm/* 1. Factorial (iterative) */long longfactorial(int n) {
long long f = 1;
for (int i = 2; i <= n; i++) f *= i;
return f; // factorial(10) = 3628800
}
/* 2. Fibonacci sequence */voidfibonacci(int count) {
int a = 0, b = 1, c;
printf("%d %d", a, b);
for (int i = 2; i < count; i++) {
c = a + b; a = b; b = c;
printf(" %d", c);
}
printf("\n");
} // fibonacci(8) prints: 0 1 1 2 3 5 8 13/* 3. Prime check (optimised - only test up to sqrt(n)) */intisPrime(int n) {
if (n < 2) return0;
if (n == 2) return1;
if (n % 2 == 0) return0;
for (int i = 3; i <= (int)sqrt(n); i += 2)
if (n % i == 0) return0;
return1;
}
/* 4. GCD - Euclidean algorithm */intgcd(int a, int b) {
while (b != 0) { int t = b; b = a % b; a = t; }
return a; // gcd(48,18) = 6
}
/* 5. Analyse a number: digits, sum, reversed, palindrome */voidanalyseNumber(int n) {
int digits=0, dSum=0, rev=0, orig=n;
while (n > 0) {
int d = n % 10;
digits++; dSum += d; rev = rev*10+d; n /= 10;
}
printf("%d: %d digits, sum=%d, reversed=%d, palindrome=%s\n",
orig, digits, dSum, rev, (orig==rev)?"yes":"no");
} // analyseNumber(12321) -> palindrome=yesintmain() {
printf("5!=%lld\n", factorial(5)); // 120fibonacci(10);
printf("17 prime=%d\n", isPrime(17)); // 1printf("GCD(48,18)=%d\n", gcd(48,18)); // 6analyseNumber(12321);
return0;
}
algorithms.c -- gcc algorithms.c -o algorithms -lm
🔀 6.10 – The goto Statement
goto unconditionally jumps to a labelled statement in the same function. It is a low-level control mechanism that is almost always avoidable. The one legitimate use-case is breaking out of deeply nested loops in a single jump — even then, a well-structured function using return is usually cleaner.
#include<stdio.h>/* goto syntax: label must be in the same function *//* goto label_name; *//* label_name: statement; *//* ── Use case: break out of nested loops ── */intmain() {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i == 2 && j == 3) {
goto done; // jump out of BOTH loops at once
}
printf("(%d,%d) ", i, j);
}
}
done:
printf("\nStopped at (2,3)\n");
return0;
}
/* ── Alternative without goto (preferred style) ── */intsearchMatrix() {
int found = 0;
for (int i = 0; i < 5 && !found; i++) {
for (int j = 0; j < 5 && !found; j++) {
if (i == 2 && j == 3) found = 1;
}
}
return found; // cleaner than goto
}
goto_example.c
Avoid goto in almost all situations. It makes code hard to read, debug, and maintain. Structured alternatives (break, continue, early return, flag variables) always exist. In professional C code, goto appears only in low-level system code and error-cleanup patterns in Linux kernel style.
Unit 6 – Lab Exercises (10 Tasks)
Write a program using a for loop to print the multiplication table of any number N entered by the user (N × 1 through N × 12). Format output as “7 x 3 = 21” with right-aligned spacing using %3d.
Use a while loop to implement a guessing game: the program picks 42, the user keeps guessing until correct. Print “Too High”, “Too Low”, or “Correct! Attempts: N”. Count attempts.
Use a do-while loop to create a text menu: (1) Check Even/Odd, (2) Find Factorial, (3) Print Fibonacci, (0) Quit. Keep showing the menu until 0 is chosen. Implement each option.
Print all prime numbers between 2 and 100 using a nested for loop. Count and display the total found. Format: 10 primes per line using %4d.
Write separate functions that print: (a) right-angle star triangle 5 rows; (b) inverted triangle; (c) number pyramid; (d) Floyd’s triangle. Call all four from main.
Using a while loop on number 12321, compute: digit count, digit sum, digit product, reversed number. Print all results and whether the number is a palindrome.
Read N integers (use a for loop). Then compute: minimum, maximum, sum, average, and count of values above average. Print all results.
Implement the Sieve of Eratosthenes to find all primes up to 200. Use an int isPrime[201] array initialised to 1; use nested loops to mark composites. Print primes 10 per line.
Print a diamond pattern of stars for an odd number N entered by the user (e.g., N=5 gives a 9-row diamond). Derive space and star counts from the row number using nested loops.
Implement Binary Search using a while loop: create a sorted array of 10 integers; ask the user for a target; repeatedly compare mid-point and halve the search range. Print each comparison step and the final result (index or “Not found”).
Hint: Lab 8 (Sieve): mark isPrime[0]=isPrime[1]=0 first; for each prime p, mark p×p, p×(p+1), … up to 200 as 0. Lab 10 (binary search): always update low = mid + 1 or high = mid - 1, never just low = mid to avoid infinite loops. Compile with gcc -std=c99 for int array VLAs if needed.
Unit 8 – Functions
Functions allow you to break code into reusable, named blocks. They are the cornerstone of structured programming in C — reducing duplication, improving readability, and enabling modular design.
🧩 Function Syntax Quick Reference
Functions in C have three common forms: prototype, definition, and call. Pointer parameters are used when the caller's data must be changed.
intadd(int a, int b);
voidswap(int *a, int *b);
intadd(int a, int b) {
return a + b;
}
result = add(2, 3);
swap(&x, &y);
📖 Theory Focus
A function boundary is both a code-organisation tool and a contract about inputs, outputs, and side effects.
C uses pass-by-value, which means plain parameters receive copies; mutation of caller data requires passing an address.
Good functions reduce cognitive load because each one captures one operation, one idea, or one business rule clearly.
🏗️ 6.1 – Function Anatomy: Prototype, Definition, Call
/*═══════════════════════════════════════════════
PROTOTYPE (Declaration) – goes at top of file
Tells compiler: "this function exists, here are
its parameter types and return type"
═══════════════════════════════════════════════*/intadd(int a, int b); // prototypevoidprintLine(int n); // prototype (void = no return)doublecircleArea(double r); // prototype/*═══════════════════════════════════════════════
FUNCTION CALL – inside main() or another function
═══════════════════════════════════════════════*/intmain() {
int result = add(5, 3); // call – 5 and 3 are "arguments"printLine(20); // call with one argumentdouble a = circleArea(7.0); // callprintf("%d, %.2f\n", result, a);
return0;
}
/*═══════════════════════════════════════════════
FUNCTION DEFINITIONS – below main()
returnType functionName(type param, ...) { body }
═══════════════════════════════════════════════*/intadd(int a, int b) { // a, b are "parameters"return a + b; // return value to caller
}
voidprintLine(int n) {
for (int i = 0; i < n; i++) printf("-");
printf("\n");
// no return needed for void
}
doublecircleArea(double r) {
return3.14159 * r * r;
}
functions.c
Function Type
Returns?
Parameters?
Example
No return, no param
No (void)
No (void)
void greet(void)
No return, with param
No
Yes
void printN(int n)
With return, no param
Yes
No
int getMax(void)
With return, with param
Yes
Yes
int add(int a, int b)
📚 6.2 – Function Call Stack (Memory Diagram)
Each function call creates a new stack frame on the stack. When the function returns, its frame is popped off.
Stack grows downward ↓
High address
Frame: main()caller
local: x = 5
5
0x7fff01a0
local: result
8 ←
0x7fff01a4
return addr
0x400…
0x7fff01a8
↓ stack grows here
Frame: add(5, 3)callee
param: a = 5
5
0x7fff0180
param: b = 3
3
0x7fff0184
return value
8
register
← popped on return
…
…
Low address
#include<stdio.h>intadd(int a, int b) {
// New stack frame created here
// a and b are COPIES of arguments
// (pass by value)int sum = a + b;
return sum;
// Frame destroyed when we return
}
intmain() {
int x = 5;
// When add() is called:// 1. Push new frame on stack// 2. Copy x=5 to param 'a'// 3. Copy 3 to param 'b'// 4. Execute add body// 5. Return value in register// 6. Pop frame off stackint result = add(x, 3);
printf("%d\n", result); // 8return0;
}
call_stack.c
⚖️ 6.3 – Pass by Value vs Pass by Reference
❌ Pass by Value (copy)
// original NOT changedvoiddoubleVal(int x) {
x = x * 2; // changes COPY only
}
intmain() {
int n = 5;
doubleVal(n);
printf("%d\n", n); // still 5!return0;
}
by_value.c
main: n
5
0x1000
copy →
doubleVal: x
5→10
0x2000
x is a separate copy. Changing x in the function does NOT affect n in main.
✅ Pass by Reference (pointer)
// original IS changedvoiddoubleRef(int *x) {
*x = (*x) * 2; // changes original
}
intmain() {
int n = 5;
doubleRef(&n); // pass ADDRESS of nprintf("%d\n", n); // 10! ✓return0;
}
by_ref.c
main: n
5→10
0x1000
*x points here
doubleRef: x
0x1000
0x2000
x holds the address of n. Writing to *x modifies n directly.
🌀 6.4 – Recursion
A recursive function calls itself with a simpler input, until a base case stops the recursion.
#include<stdio.h>int globalVar = 100; // GLOBAL: visible everywhere in filevoidcounter() {
staticint count = 0; // STATIC: persists between calls
count++; // NOT reset on each callprintf("Called %d times\n", count);
}
voidscopeDemo() {
int localVar = 50; // LOCAL: only in this functionprintf("local=%d, global=%d\n", localVar, globalVar);
// globalVar is accessible here
globalVar++;
}
intmain() {
scopeDemo(); // local=50, global=100printf("global=%d\n", globalVar); // 101 (modified in scopeDemo)counter(); // Called 1 timescounter(); // Called 2 times (static persists!)counter(); // Called 3 times// Block scope
{
int blockVar = 99; // only in this blockprintf("blockVar=%d\n", blockVar);
}
// blockVar is GONE herereturn0;
}
scope.c
A function prototype is a declaration that tells the compiler about a function's name, return type, and parameter types before its full definition. Without a prototype, calling a function defined later in the file (or in another file) causes a compiler warning or error under strict standards.
⚠️ No prototype — order-dependent
// greet() is defined AFTER main()
// Without a prototype, C assumes it
// returns int — wrong! Compiler warns.intmain() {
greet(); // implicit declarationreturn0;
}
voidgreet() {
printf("Hello!\n");
}
✅ With prototype — order-free
// Declare BEFORE use so compiler
// knows the type signaturevoidgreet(void); // prototypeintmain() {
greet(); // compiler is happy ✓return0;
}
voidgreet(void) {
printf("Hello!\n");
}
For larger programs, prototypes are placed in header files (.h) so they can be shared across multiple .c source files:
/* ── math_utils.h ── */#ifndef MATH_UTILS_H
#define MATH_UTILS_H
intadd(int a, int b);
doubleaverage(int *arr, int n);
intfactorial(int n);
#endif/* MATH_UTILS_H */math_utils.h
/* ── math_utils.c ── */#include"math_utils.h"intadd(int a, int b) {
return a + b;
}
doubleaverage(int *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return (double)sum / n;
}
intfactorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
math_utils.c
Include guards (#ifndef HEADER_H / #define HEADER_H / #endif) prevent a header from being included twice in the same translation unit, which would cause duplicate-definition errors.
⚡ 6.6 – Macros vs Functions vs inline
C gives you three ways to factor out reusable computation. Each has different trade-offs for type safety, debugging, and performance.
#include<stdio.h>/* ── 1. Macro: text substitution by preprocessor ── */#define SQUARE_MACRO(x) ((x) * (x)) // always parenthesise!// Pitfall: SQUARE_MACRO(i++) expands to ((i++)*(i++)) — UB!/* ── 2. Regular function: type-safe, call overhead ── */intsquare_fn(int x) {
return x * x;
}
// Works correctly: square_fn(i++) is well-defined/* ── 3. inline function: hint to compiler to expand at call site ── */staticinlineintsquare_inline(int x) {
return x * x;
}
// Type-safe like a function, but may avoid function-call overhead// 'static' keeps it file-local to avoid multiple-definition issues// With optimisation (-O2), the compiler often inlines anywayintmain() {
printf("%d\n", SQUARE_MACRO(5)); // 25 (preprocessor expands)printf("%d\n", square_fn(5)); // 25 (function call)printf("%d\n", square_inline(5)); // 25 (likely inlined)return0;
}
macro_vs_function.c
Macro
Function
inline function
Type checking
❌ None
✅ Full
✅ Full
Side-effect safe
❌ Dangerous
✅ Safe
✅ Safe
Debuggable in GDB
❌ No
✅ Yes
✅ Usually
Call overhead
None (text sub)
Possible
None (if inlined)
Works with all types
✅ (duck typing)
❌ One type set
❌ One type set
Prefer static inline functions over function-like macros in C. Use macros only for constants (#define MAX 100) or genuinely type-generic operations where you cannot use a function.
Unit 8 – Lab Exercises
Write four separate functions: findMax(a,b), findMin(a,b), square(n), cube(n). Call all four from main and print results.
Write a function isPrime(int n) that returns 1 if prime, else 0. Use it to print all prime numbers between 1 and 100.
Write a recursive function power(base, exp) that computes base^exp without using pow().
Write a swap function that works correctly using pointers: void swap(int *a, int *b). Demonstrate that the values in main are actually swapped.
Demonstrate the static variable counter: write function hitCounter() that prints how many times it's been called. Call it 5 times from main.
Write a function sumDigits(int n) that recursively sums the digits of a positive integer (e.g., sumDigits(1234) = 10).
Hint: For sumDigits recursively: base case = n < 10, recursive case = (n % 10) + sumDigits(n / 10).
Unit 7 – Arrays & Strings
Arrays store multiple values of the same type in contiguous memory. Strings in C are null-terminated character arrays. Mastering arrays is essential before tackling pointers and data structures.
🗃️ Array and String Syntax
Array syntax fixes the element type and length. String syntax is special because a C string is just a char array ending in '\0'.
int arr[5] = {1, 2, 3, 4, 5};
char name[20] = "Linux";
int matrix[2][3] = {{1,2,3},{4,5,6}};
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
fgets(name, sizeof(name), stdin);
📖 Theory Focus
Arrays are efficient because elements live contiguously in memory, which makes indexing and traversal predictable.
Strings are more fragile because the language does not store the length automatically; library functions rely on the null terminator.
The step from arrays to pointers becomes easier once you see that indexing is really address arithmetic on contiguous storage.
📦 7.1 – 1D Arrays: Memory Layout
An array occupies consecutive memory cells. Each element has the same size (e.g., 4 bytes for int).
Memory layout of int arr[5] = {10, 20, 30, 40, 50};
arr[0]
10
1000
arr[1]
20
1004
arr[2]
30
1008
arr[3]
40
1012
arr[4]
50
1016
← Each int = 4 bytes. Address = base + (index × sizeof(int))
#include<stdio.h>intmain() {
/* ── Declaration and Initialization ── */int arr[5] = {10, 20, 30, 40, 50};
/* ── Access by index (0-based) ── */printf("First: %d\n", arr[0]); // 10printf("Last: %d\n", arr[4]); // 50/* ── Traverse with for loop ── */int sum = 0;
for (int i = 0; i < 5; i++) {
sum += arr[i];
printf("arr[%d] = %d addr = %p\n", i, arr[i], &arr[i]);
}
printf("Sum = %d\n", sum); // 150/* ── Partial initialization: rest = 0 ── */int scores[10] = {90, 85}; // scores[2..9] = 0 automatically/* ── Size using sizeof ── */int len = sizeof(arr) / sizeof(arr[0]); // 20/4 = 5printf("Length = %d\n", len);
return0;
}
arrays1d.c
🗂️ 7.2 – 2D Arrays: Row-Major Storage
C stores 2D arrays in row-major order — all elements of row 0 come first in memory, then row 1, etc.
int matrix[2][3] = {{1,2,3},{4,5,6}} — row-major memory layout:
[0][0]
1
1000
[0][1]
2
1004
[0][2]
3
1008
[1][0]
4
1012
[1][1]
5
1016
[1][2]
6
1020
Blue = row 0 · Green = row 1 · Address formula: base + (row × cols + col) × sizeof(type)
#include<stdio.h>intmain() {
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
/* ── Print matrix ── */for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++)
printf("%3d", matrix[r][c]);
printf("\n");
}
/* ── Main diagonal sum ── */int diagSum = 0;
for (int i = 0; i < 3; i++)
diagSum += matrix[i][i];
printf("Diagonal sum: %d\n", diagSum); // 1+5+9 = 15/* ── Transpose ── */int trans[3][3];
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
trans[c][r] = matrix[r][c];
return0;
}
matrix.c
🔠 7.3 – Character and String in C
This section focuses on strings in detail: how they are stored in memory, how to read them safely, and how to process them character by character.
Item
Meaning
Example
Important Note
Character
Single byte value in char
char ch = 'A';
Stored as ASCII code ('A' = 65)
String
Array of chars ending with '\0'
char s[] = "HELLO";
Compiler adds terminator automatically
Length
Number of visible characters
strlen("HELLO")
Returns 5, excludes '\0'
Safe input
Limit input width
scanf("%19s", s)
Prevents buffer overflow
Line input
Read full line with spaces
fgets(s, sizeof(s), stdin)
Safer than gets (never use gets)
char word[] = "CAT"; memory layout:
word[0]
'C'
67
word[1]
'A'
65
word[2]
'T'
84
word[3]
'\0'
0
The final '\0' byte is mandatory. Without it, string functions keep reading memory and cause undefined behavior.
Common mistakes to avoid: (1) forgetting '\0', (2) using %s without width in scanf, (3) copying into small buffers with strcpy, (4) modifying string literals like char *p = "abc"; p[0] = 'A'; (undefined behavior).
🔤 7.4 – Strings: Null-Terminated Character Arrays
In C, a string is a char array ending with the null terminator '\0' (ASCII 0). This is the sentinel that marks the end of the string.
char name[] = "Hello"; — stored in memory as:
name[0]
'H'
72
name[1]
'e'
101
name[2]
'l'
108
name[3]
'l'
108
name[4]
'o'
111
name[5]
'\0'
0
The '\0' null terminator is REQUIRED — it's how functions like printf and strlen know where the string ends.
Buffer overflow risk: Always use strncpy instead of strcpy, and %19s (limit) instead of %s with scanf to prevent writing past the array boundary.
🔗 7.5 – Array Decay to Pointer
In most expressions, an array name decays (is implicitly converted) into a pointer to its first element. This is one of the most important — and most confusing — rules in C. Understanding it clarifies why you cannot find the length of an array inside a function that received it as a parameter.
#include<stdio.h>intmain() {
int arr[5] = {10, 20, 30, 40, 50};
/* sizeof(arr) returns the full array size (before decay) */printf("sizeof(arr) = %zu bytes\n", sizeof(arr)); // 20printf("sizeof(arr[0])= %zu bytes\n", sizeof(arr[0])); // 4printf("Length = %zu\n", sizeof(arr) / sizeof(arr[0])); // 5/* When arr is used in most expressions, it decays to int * */int *p = arr; // arr decays to &arr[0]printf("arr = %p\n", (void*)arr); // address of first elementprintf("p = %p\n", (void*)p); // same addressprintf("arr[2] = %d, *(arr+2) = %d\n", arr[2], *(arr+2)); // both 30/* Decay happens when passed to a function */return0;
}
/* Function receives int* not the whole array.
sizeof(a) here would give sizeof(int*) = 8 bytes, NOT 20! */voidprocessArray(int *a, int n) {
printf("sizeof(a) in function = %zu\n", sizeof(a)); // 8 (pointer size!)printf("n (passed explicitly) = %d\n", n); // 5 (correct)for (int i = 0; i < n; i++)
printf("%d ", a[i]); // a[i] == *(a+i)
}
array_decay.c
Summary: When does decay NOT happen?
Context
Decays?
Why
sizeof(arr)
❌ No
sizeof is a special operator that sees the full type
&arr
❌ No
Address-of applied to the whole array, gives int(*)[5]
arr[i], arr + 1
✅ Yes
Used as a value expression — decays to int *
Passed to function
✅ Yes
Function receives a pointer to first element
Assigned to pointer
✅ Yes
int *p = arr; — decay occurs
Always pass the length separately when passing arrays to functions. There is no way to recover the array length from a decayed pointer alone.
🛡️ 7.6 – Safe String Input & <ctype.h> Character Functions
Reading strings safely in C requires understanding why some functions are dangerous and knowing how to use alternatives correctly.
#include<stdio.h>#include<string.h>#include<ctype.h>intmain() {
char name[50];
/* ── DANGEROUS: gets() — NEVER use ── */// gets(name); // removed from C11 standard — buffer overflow risk!// If user types more than 49 chars → stack corruption → crash/attack/* ── SAFE: fgets() — always specify buffer size ── */printf("Enter name: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
printf("Input error\n");
return1;
}
/* fgets keeps the newline '\n' — strip it */
name[strcspn(name, "\n")] = '\0'; // elegant removal of trailing \nprintf("Hello, %s!\n", name);
/* ── scanf with width limit — safer than no limit ── */char word[20];
scanf("%19s", word); // reads max 19 chars + null terminator/* ── Parse from string with sscanf ── */char line[] = "Alice 25 9.5";
char person[20]; int age; float score;
sscanf(line, "%19s %d %f", person, &age, &score);
printf("%s is %d years old, score %.1f\n", person, age, score);
return0;
}
safe_input.c
Character Classification Functions from <ctype.h>:
Function
Returns true (non-zero) if c is…
Example
isalpha(c)
A letter (a–z, A–Z)
isalpha('A') → 1
isdigit(c)
A decimal digit (0–9)
isdigit('7') → 1
isalnum(c)
Letter or digit
isalnum('z') → 1
isspace(c)
Whitespace: space, tab, newline
isspace('\n') → 1
isupper(c)
Uppercase letter (A–Z)
isupper('Z') → 1
islower(c)
Lowercase letter (a–z)
islower('a') → 1
ispunct(c)
Punctuation character
ispunct('.') → 1
toupper(c)
—
Returns uppercase version of c
tolower(c)
—
Returns lowercase version of c
#include<stdio.h>#include<ctype.h>/* Count vowels, consonants, digits, spaces in a string */voidanalyseString(constchar *s) {
int vowels=0, consonants=0, digits=0, spaces=0;
for (; *s; s++) {
char c = (char)tolower((unsigned char)*s);
if (isdigit((unsigned char)*s)) digits++;
else if (isspace((unsigned char)*s)) spaces++;
else if (isalpha((unsigned char)*s)) {
if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') vowels++;
else consonants++;
}
}
printf("Vowels=%d Consonants=%d Digits=%d Spaces=%d\n",
vowels, consonants, digits, spaces);
}
ctype_demo.c
Always cast the character argument to unsigned char before passing to <ctype.h> functions: isdigit((unsigned char)c). Passing a plain char can cause undefined behaviour on systems where char is signed and the value is negative (e.g., non-ASCII input).
Unit 7 – Lab Exercises
Declare an array of 10 integers from user input. Find the max, min, and average.
Write a function void reverseArray(int *arr, int n) that reverses an array in-place.
Implement linear search: int linearSearch(int *arr, int n, int key) — return index or -1.
Implement bubble sort on an array of 10 integers. Print the sorted array.
Read a string and count: (a) vowels, (b) consonants, (c) digits, (d) spaces.
Write a function that reverses a string in-place without using <string.h>.
Check if a string is a palindrome (e.g., "madam", "racecar" are palindromes).
Multiply two 2×2 matrices and print the result matrix.
Hint: For matrix multiplication C[i][j] += A[i][k] * B[k][j] with a triple nested loop.
Unit 9 – Pointers
Pointers are variables that store memory addresses. They are the most powerful (and dangerous) feature of C, enabling direct memory manipulation, efficient array handling, and dynamic memory allocation.
🧭 Pointer Syntax Quick Reference
Pointer syntax has three core operators: declaration with *, address-of with &, and dereference with * while reading or writing through the pointer.
int x = 10;
int *p = &x;
printf("%d %p %d\n", x, p, *p);
*p = 25;
voidupdate(int *value) {
*value += 1;
}
📖 Theory Focus
A pointer is meaningful only when you separate the address it stores from the data located at that address.
Pointers matter because C lets programs share, mutate, and traverse memory directly instead of hiding it behind higher-level abstractions.
Most pointer bugs come from invalid lifetime, wrong type assumptions, or dereferencing a value that is null, dangling, or out of bounds.
🎯 8.1 – Pointer Basics: Addresses and Dereference
Memory diagram: int x = 42; int *p = &x;
int x
42
0x1000
p stores 0x1000
int *p
0x1000
0x2000
&x = "address of x" = 0x1000 |
*p = "value at address p" = 42 |
p == &x is true
#include<stdio.h>intmain() {
int x = 42;
int *p = &x; // p holds the ADDRESS of xprintf("x = %d\n", x); // 42printf("&x = %p\n", &x); // address (e.g. 0x7fff1000)printf("p = %p\n", p); // same address as &xprintf("*p = %d\n", *p); // 42 — dereference: value AT p
*p = 99; // modify x THROUGH the pointerprintf("x = %d\n", x); // 99 — x changed!/* ── Pointer to double ── */double d = 3.14;
double *pd = &d;
printf("sizeof(pd) = %zu\n", sizeof(pd)); // 8 on 64-bit (all pointers same size)printf("*pd = %.2f\n", *pd); // 3.14return0;
}
pointers.c
➕ 8.2 – Pointer Arithmetic
When you add 1 to a pointer, it advances by sizeof(type) bytes — not just 1 byte.
#include<stdio.h>intmain() {
int arr[] = {10, 20, 30, 40};
int *ptr = arr; // arr decays to pointer to arr[0]/* ── Using pointer arithmetic to traverse ── */for (int i = 0; i < 4; i++) {
printf("*(ptr+%d) = %d addr=%p\n", i, *(ptr+i), (ptr+i));
}
/* arr[i] and *(arr+i) are IDENTICAL *//* ── Pointer increment ── */printf("%d\n", *ptr); // 10
ptr++;
printf("%d\n", *ptr); // 20 (advanced 4 bytes)
ptr++;
printf("%d\n", *ptr); // 30/* ── Pointer difference ── */int *p1 = &arr[0];
int *p2 = &arr[3];
printf("diff = %td\n", p2 - p1); // 3 (number of elements)return0;
}
ptr_arith.c
🔗 8.3 – Pointer–Array Equivalence
/* arr[i] ≡ *(arr + i)
&arr[i] ≡ arr + i
When passed to a function, array DECAYS to pointer */// Printing a string with a char pointerchar str[] = "Linux";
char *cp = str;
while (*cp != '\0') {
printf("%c", *cp);
cp++;
}
printf("\n"); // Linux// Passing array to function: arrives as pointervoidsumArray(int *arr, int n) { // same as int arr[]int s = 0;
for (int i = 0; i < n; i++) s += arr[i];
printf("sum = %d\n", s);
}
// String literals are read-only pointerschar *literal = "Hello"; // stored in read-only memory// literal[0] = 'X'; // CRASH: cannot modify!char array[] = "Hello"; // stored on stack — writable
array[0] = 'X'; // OK: "Xello"ptr_array.c
🧩 8.4 – Double Pointers & Pointer to Pointer
int x=5; int *p=&x; int **pp=&p;
int x
5
0x100
p = &x
int *p
0x100
0x200
pp = &p
int **pp
0x200
0x300
int x = 5;
int *p = &x; // p points to xint **pp = &p; // pp points to pprintf("%d\n", x); // 5printf("%d\n", *p); // 5printf("%d\n", **pp); // 5 (double dereference)
**pp = 99;
printf("%d\n", x); // 99 (modified through **pp)// Useful for: argv (char **argv), modifying a pointer in a functiondouble_ptr.c
🛡️ 8.5 – NULL Pointers & Safe Pointer Practices
#include<stdio.h>#include<stdlib.h>intmain() {
/* ── NULL pointer: points to nothing ── */int *p = NULL; // good practice: initialize to NULL/* ── Always check before dereferencing ── */if (p != NULL) {
printf("%d\n", *p); // safe
} else {
printf("p is NULL, cannot dereference!\n");
}
/* ── Dynamic allocation (preview of heap) ── */int *arr = (int*) malloc(5 * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "malloc failed!\n");
return1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 10;
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
free(arr); // ALWAYS free! avoids memory leak
arr = NULL; // avoid dangling pointerprintf("\n");
return0;
}
null_ptr.c
malloc / calloc / realloc / free
Purpose
malloc(n)
Allocate n bytes (uninitialized)
calloc(count, size)
Allocate count×size bytes, zero-initialized
realloc(ptr, newsize)
Resize existing allocation
free(ptr)
Release back to heap (must call once per malloc)
Common pointer bugs: (1) Null dereference — always check NULL before using. (2) Dangling pointer — set to NULL after free. (3) Buffer overrun — don't go past array bounds. (4) Memory leak — always pair malloc with free.
🌐 8.5 – void *: The Generic Pointer
A void * pointer can hold the address of any data type. It is the C mechanism for generic (type-agnostic) code. You cannot dereference a void * directly — you must first cast it to a concrete type. The standard library uses void * extensively (e.g., malloc, memcpy, qsort).
#include<stdio.h>#include<stdlib.h>intmain() {
int i = 42;
double d = 3.14;
char ch = 'A';
void *vp; // generic pointer — holds any address
vp = &i; // point at intprintf("int via void*: %d\n", *(int*)vp); // cast needed
vp = &d; // point at doubleprintf("double via void*: %.2f\n", *(double*)vp);
vp = &ch; // point at charprintf("char via void*: %c\n", *(char*)vp);
/* malloc returns void* — you cast it to the needed type */int *arr = (int*) malloc(5 * sizeof(int));
if (!arr) { perror("malloc"); return1; }
for (int k = 0; k < 5; k++) arr[k] = k * k;
for (int k = 0; k < 5; k++) printf("%d ", arr[k]); // 0 1 4 9 16free(arr);
return0;
}
void_ptr.c
void * in C does not mean "any object" in the OOP sense. It is purely a type-erased pointer. You are responsible for remembering what type it actually points to — the compiler will not check.
🔒 8.6 – const Qualifiers with Pointers
The position of const in a pointer declaration controls what is constant: the data pointed to, or the pointer itself, or both. This is one of the most commonly misunderstood aspects of C.
#include<stdio.h>intmain() {
int x = 10, y = 20;
/* ── 1. Pointer to const int: data is read-only ── */constint *p1 = &x; // "const int" = cannot change *p1// *p1 = 99; // ❌ compile error: read-only data
p1 = &y; // ✅ OK: can change the pointer itselfprintf("*p1 = %d\n", *p1); // 20/* ── 2. Const pointer to int: pointer is fixed ── */int * const p2 = &x; // * const = pointer address cannot change
*p2 = 99; // ✅ OK: can change the dataprintf("x = %d\n", x); // 99// p2 = &y; // ❌ compile error: const pointer/* ── 3. Const pointer to const int: nothing changes ── */constint * const p3 = &x;
// *p3 = 50; // ❌ data is const// p3 = &y; // ❌ pointer is constprintf("*p3 = %d\n", *p3); // read-only viewreturn0;
}
const_pointers.c
Declaration
Read pointer p?
Change *p (data)?
Change p (address)?
int *p
✅
✅
✅
const int *p
✅
❌
✅
int * const p
✅
✅
❌
const int * const p
✅
❌
❌
Mnemonic — read right to left:const int *p = "p is a pointer to int which is const". int * const p = "p is a const pointer to int". Use const int * for function parameters that should not modify the pointed-to data (e.g., void printName(const char *name)).
Stack memory is automatically managed and limited in size. Heap memory is allocated at runtime with explicit functions, persists until you free it, and can be as large as available RAM (subject to OS limits). Every malloc/calloc/realloc must have a matching free.
#include<stdio.h>#include<stdlib.h>#include<string.h>intmain() {
int n;
printf("How many integers? ");
scanf("%d", &n);
/* ── malloc: allocate n*4 bytes, UNINITIALISED ── */int *arr = (int*) malloc(n * sizeof(int));
if (arr == NULL) { perror("malloc"); return1; }
for (int i = 0; i < n; i++) arr[i] = i + 1;
/* ── calloc: allocate n*4 bytes, ZERO-INITIALISED ── */int *zarr = (int*) calloc(n, sizeof(int)); // count, size_eachif (zarr == NULL) { perror("calloc"); free(arr); return1; }
printf("zarr[0] = %d (guaranteed 0)\n", zarr[0]);
/* ── realloc: resize arr to hold 2*n elements ── */int *bigger = (int*) realloc(arr, 2 * n * sizeof(int));
if (bigger == NULL) {
perror("realloc");
free(zarr); free(arr); return1;
}
arr = bigger; // realloc may move memory! always use returned ptrfor (int i = n; i < 2*n; i++) arr[i] = (i+1) * 10;
for (int i = 0; i < 2*n; i++) printf("%d ", arr[i]);
printf("\n");
/* ── free: release memory back to heap ── */free(arr); arr = NULL; // set to NULL to avoid dangling pointerfree(zarr); zarr = NULL;
return0;
}
dynamic_memory.c
Common Dynamic Memory Bugs
Bug
What happens
Prevention
Memory leak
Never calling free() — heap fills up
Every malloc must have a free; use Valgrind
Double free
Calling free(p) twice — heap corruption
Set pointer to NULL after free
Use after free
Accessing memory after free() — undefined behaviour
Set pointer to NULL and check before use
Buffer overrun
Writing past end of allocated block — corruption
Track allocated size, use realloc to grow
Ignoring NULL return
Dereferencing NULL when malloc fails — crash
Always if (!ptr) { handle error }
Never use the original pointer after realloc:realloc may move the memory block to a new address and free the old one. Always assign the return value to a (possibly new) pointer variable and check it for NULL before updating your main pointer.
Unit 9 – Lab Exercises
Write a program that uses a pointer to traverse an array of ints and prints each element and its address. Observe the 4-byte gap between addresses.
Implement void swap(int *a, int *b) using pointers. Verify the swap in main.
Write a function int* findMax(int *arr, int n) that returns a pointer to the maximum element in the array. Print the value and its address.
Use pointer arithmetic (no array indexing []) to reverse an array in-place.
Dynamically allocate an array of N integers using malloc. Fill it with squares (1, 4, 9, …). Print it. Free it properly.
Write a function char* myStrcat(char *dest, const char *src) that does string concatenation using pointer arithmetic only (no <string.h> functions).
Hint: For exercise 6, advance the dest pointer to '\0' first, then copy characters from src one by one, then append '\0'.
Unit 13 – Pointers and Structures
Pointers and structures are a core pair in C systems programming. This unit focuses on struct pointers, arrow-operator access, dynamic arrays of structs, and linked data structures.
🔗 Core Equivalences
Struct member access via dot and pointer access via arrow are equivalent forms:
#include<stdio.h>#include<stdlib.h>typedefstruct {
int id;
char name[24];
} Employee;
intmain() {
int n = 3;
Employee *emp = (Employee*) malloc(n * sizeof(Employee));
if (!emp) return1;
emp[0] = (Employee){1, "Asha"};
emp[1] = (Employee){2, "Bharat"};
emp[2] = (Employee){3, "Charan"};
for (int i = 0; i < n; i++) {
printf("%d %s\n", (emp + i)->id, (emp + i)->name);
}
free(emp);
return0;
}
dynamic_struct_array.c
Safety: Always verify allocation result, and free() memory after use to avoid leaks.
Unit 13 – Lab Exercises
Implement myLen(const char *s) using pointer difference only.
Write myStrcmp(const char *a, const char *b) using pointer traversal.
Create a struct Student and print all fields using a pointer and the -> operator.
Write a function void updateSalary(Employee *e, float percent) to update struct data by reference.
Dynamically allocate an array of 5 structs, fill them, display them, then free memory.
Unit 12 – Structures & Unions
Structures (struct) let you group variables of different types under one name. Unions share the same memory for all members. Together they are the foundation of complex data modelling in C.
🏷️ Aggregate Type Syntax
These topics introduce the syntax for user-defined data shapes: records with struct, shared-memory variants with union, and named integer states with enum.
struct Student { int roll; char name[20]; };
union Data { int i; float f; };
enum Day { MON = 1, TUE, WED };
struct Student s = {101, "Alice"};
s.roll = 102;
ps->roll = 103;
📖 Theory Focus
A struct models a real entity by grouping related fields that should exist together at the same time.
A union models alternative representations of the same memory, which is useful when only one interpretation is active at once.
Layout, alignment, and padding are not trivia here; they directly affect binary size, protocol mapping, and interoperability.
🏛️ 9.1 – Defining and Using Structures
#include<stdio.h>#include<string.h>/* ── Define the struct type ── */struct Student {
int rollNo;
char name[50];
float cgpa;
};
/* ── typedef for convenience ── */typedefstruct {
char model[30];
int year;
float price;
} Car;
intmain() {
/* ── Declare and initialize ── */struct Student s1 = {101, "Alice", 9.2};
/* ── Access with dot operator ── */printf("Roll: %d Name: %s CGPA: %.1f\n",
s1.rollNo, s1.name, s1.cgpa);
/* ── Modify a member ── */
s1.cgpa = 9.4;
strcpy(s1.name, "Bob");
/* ── Using typedef ── */
Car c1 = {"Tesla Model 3", 2024, 42000.0};
printf("%s (%d) = $%.0f\n", c1.model, c1.year, c1.price);
/* ── Array of structs ── */struct Student roster[3] = {
{101, "Alice", 9.2},
{102, "Bob", 8.7},
{103, "Carol", 9.5}
};
for (int i = 0; i < 3; i++)
printf("%d %s %.1f\n",
roster[i].rollNo, roster[i].name, roster[i].cgpa);
return0;
}
structs.c
📐 9.2 – Structure Memory Layout & Padding
The compiler inserts padding bytes between struct members to align each member to its natural alignment boundary (usually its size). This is called structure padding.
struct Padded { char a; int b; char c; }; — total 12 bytes (not 6!)
#include<stdio.h>struct Padded { char a; int b; char c; }; // 12 bytesstruct Reorder { char a; char c; int b; }; // 8 bytesintmain() {
printf("Padded = %zu bytes\n", sizeof(struct Padded)); // 12printf("Reorder = %zu bytes\n", sizeof(struct Reorder)); // 8// Rule: order members from LARGEST to SMALLEST type to reduce paddingreturn0;
}
padding.c
➡️ 9.3 – Pointers to Structures: Arrow Operator
#include<stdio.h>typedefstruct {
int x, y;
char label[10];
} Point;
voidprintPoint(const Point *p) {
/* (*p).x is the same as p->x */printf("Point %s: (%d, %d)\n", p->label, p->x, p->y);
}
voidtranslate(Point *p, int dx, int dy) {
p->x += dx; // modify struct through pointer
p->y += dy;
}
intmain() {
Point pt = {3, 7, "A"};
printPoint(&pt); // Point A: (3, 7)translate(&pt, 10, -2);
printPoint(&pt); // Point A: (13, 5)/* ── Pointer to struct heap allocation ── */
Point *heap_pt = (Point*) malloc(sizeof(Point));
heap_pt->x = 100;
heap_pt->y = 200;
strcpy(heap_pt->label, "B");
printPoint(heap_pt);
free(heap_pt);
return0;
}
struct_ptr.c
ptr->member is syntactic sugar for (*ptr).member. Always prefer the arrow notation when using pointers to structs.
🌳 9.4 – Nested Structures & Self-Referential (Linked List Node)
/* ── Nested struct ── */typedefstruct {
int day, month, year;
} Date;
typedefstruct {
int id;
char name[40];
Date joined; // nested struct
} Employee;
/* Access: emp.joined.day */
Employee emp = {1, "Alice", {15, 6, 2020}};
printf("%s joined on %d/%d/%d\n",
emp.name, emp.joined.day, emp.joined.month, emp.joined.year);
/* ── Self-referential: singly linked list node ── */typedefstruct Node {
int data;
struct Node *next; // pointer to SAME struct type
} Node;
/* Build: head → 1 → 2 → 3 → NULL */
Node n3 = {3, NULL};
Node n2 = {2, &n3};
Node n1 = {1, &n2};
Node *head = &n1;
/* Traverse */for (Node *cur = head; cur != NULL; cur = cur->next)
printf("%d ", cur->data); // 1 2 3nested.c
🔀 9.5 – Unions: Shared Memory
All members of a union share the same memory location. The union's size equals the largest member.
union Data { int i; float f; char str[4]; }; — all share the same 4 bytes
int i (4 bytes)
float f (4 bytes)
char str[4]
All three members overlap at the same memory address. Writing one overwrites the others.
#include<stdio.h>union Data {
int i;
float f;
char str[4];
};
intmain() {
union Data d;
printf("sizeof(union Data) = %zu\n", sizeof(d)); // 4
d.i = 65;
printf("d.i = %d\n", d.i); // 65printf("d.f = %f\n", d.f); // some float (garbage from int bits)printf("d.str= %c\n", d.str[0]); // 'A' (65 = ASCII 'A')
d.f = 3.14f;
printf("d.f = %.2f\n", d.f); // 3.14printf("d.i = %d\n", d.i); // raw bits of 3.14f as int// Only the LAST member written is valid!return0;
}
unions.c
struct
union
Memory
Each member has own storage
All members share one storage
Size
Sum of all members + padding
Size of largest member
Usage
Store multiple values at once
Store ONE value at a time
Typical use
Records, data modelling
Type tagging, memory mapping
🏷️ 9.6 – Enumerations (enum): Named Integer Constants
An enum defines a set of named integer constants that make code more readable and self-documenting. Internally, enum values are integers — the first is 0 by default, then each subsequent member is one more than the previous. You can assign explicit values.
#include<stdio.h>/* ── Basic enum ── */enum Day {
MON=1, TUE, WED, THU, FRI, SAT, SUN // TUE=2, WED=3, etc.
};
/* ── typedef for cleaner usage ── */typedefenum {
RED, GREEN, BLUE // RED=0, GREEN=1, BLUE=2
} Colour;
/* ── Bitmask enum (powers of 2) ── */typedefenum {
PERM_NONE = 0, // 0000
PERM_READ = 1, // 0001
PERM_WRITE = 2, // 0010
PERM_EXEC = 4// 0100 (combine with bitwise OR)
} Permission;
intmain() {
enum Day today = WED;
printf("Wednesday is day %d\n", today); // 3
Colour c = GREEN;
switch (c) {
case RED: printf("Red\n"); break;
case GREEN: printf("Green\n"); break;
case BLUE: printf("Blue\n"); break;
}
/* Bitmask: combine permissions with OR */
Permission user = PERM_READ | PERM_WRITE; // 0011if (user & PERM_READ) printf("Can read\n");
if (user & PERM_WRITE) printf("Can write\n");
if (!(user & PERM_EXEC)) printf("Cannot execute\n");
return0;
}
enum_demo.c
enum
#define constants
Type safety
✅ Has a type, debugger-visible
❌ Just text substitution
Scope awareness
✅ Respects C scope rules
❌ Global, no scope
Auto-increment
✅ Each member = previous + 1
❌ Must specify every value
Switch coverage check
✅ -Wswitch warns on missed cases
❌ No such warning
🔢 9.7 – Bit Fields in Structures
A bit field is a struct member that specifies how many bits it should occupy. Bit fields allow extremely compact data representation — useful in embedded systems, protocol headers, and flag registers where every bit has a distinct meaning.
#include<stdio.h>/* ── Regular struct: uses 3 bytes minimum ── */struct Flags_Regular {
unsigned char isActive; // 1 byte (uses only 1 bit logically)unsigned char isAdmin; // 1 byteunsigned char isBanned; // 1 byte
}; // sizeof = 3 bytes/* ── Bit field struct: packs everything into 1 byte ── */struct Flags_Bits {
unsigned int isActive : 1; // 1 bit → 0 or 1unsigned int isAdmin : 1; // 1 bitunsigned int isBanned : 1; // 1 bitunsigned int level : 4; // 4 bits → values 0–15unsigned int unused : 1; // 1 bit padding to fill byte
}; // sizeof = 4 bytes (one int), but only 8 bits used/* ── Practical example: CPU flags register / network header ── */typedefstruct {
unsigned int cf : 1; // Carry flagunsigned int : 1; // reserved (unnamed bit)unsigned int pf : 1; // Parity flagunsigned int zf : 1; // Zero flagunsigned int sf : 1; // Sign flagunsigned int of : 1; // Overflow flag
} CpuFlags;
intmain() {
struct Flags_Regular r;
struct Flags_Bits b;
printf("Regular flags: %zu bytes\n", sizeof(r)); // 3printf("Bit field flags: %zu bytes\n", sizeof(b)); // 4 (one int)
b.isActive = 1;
b.isAdmin = 0;
b.isBanned = 0;
b.level = 7; // 4 bits → max 15printf("Active=%u Admin=%u Banned=%u Level=%u\n",
b.isActive, b.isAdmin, b.isBanned, b.level);
/* Attempting to store out-of-range value wraps silently */
b.level = 20; // 20 = 10100b, 4-bit field stores 0100b = 4printf("Level after 20 (overflow): %u\n", b.level); // 4return0;
}
bitfields.c
Bit field limitations: (1) You cannot take the address of a bit field member (&b.level is an error). (2) The layout in memory is implementation-defined — bit fields are not portable across compilers for network protocols or binary file formats; use explicit bit masking instead. (3) Bit fields are most useful in embedded systems for hardware register mapping where the compiler and platform are known and fixed.
Unit 12 – Lab Exercises
Define a struct Book with fields: title (50 chars), author (40 chars), year (int), price (float). Create an array of 3 books, fill from user input, and print them sorted by year.
Write a function void printStudent(struct Student *s) that takes a pointer to struct and prints all fields using the arrow operator.
Print the sizeof for at least 3 different struct layouts. Observe how member ordering affects total size.
Design a simple linked list with at least 4 nodes. Write functions to: (a) print all nodes, (b) find a node by value, (c) count nodes.
Create a union that can hold either a float sensor value or a char[4] sensor code. Demonstrate writing and reading each member.
Unit 14 – File Handling
File I/O in C uses the FILE* pointer and standard library functions (<stdio.h>). You can read and write both text and binary files. Always check for null FILE* and close files when done.
📄 File I/O Syntax Quick Reference
File programs follow a strict lifecycle: open, validate, read or write, then close. The main syntax difference is whether you use text functions or binary functions.
Never use feof() as a loop condition — it only becomes true after a failed read, causing off-by-one errors. Use the return value of fgets/fread/fscanf as the loop condition instead.
Unit 14 – Lab Exercises
Write a program to copy one text file to another (file copy utility). Read and write line by line.
Count the number of lines, words, and characters in a text file (like the Linux wc command).
Create a student record management system using binary files: support add, list, and search by roll number operations. Persist data to students.bin.
Write a program that reads a CSV file (name,age,score per line) and computes the average score. Use fscanf with "," delimiters or fgets + sscanf.
Implement a simple append-based log file: each run appends a timestamped entry. Use time.h for the timestamp.
Hint: For timestamped log: #include <time.h>, then time_t t = time(NULL); fprintf(fp, "%s", ctime(&t));
Unit 15 – Debugging & Best Practices
Writing code that compiles is the easy part. This unit teaches you to find and fix bugs systematically using GCC warnings, GDB debugger, Valgrind memory checker, and good coding practices.
🛠️ Debugging Command Syntax
Debugging has its own operating syntax: compile with symbols, run with a debugger, inspect state, and use memory tools to confirm leaks or invalid accesses.
gcc -std=c11 -Wall -Wextra -g prog.c -o prog
gdb ./prog
break main
run
print value
backtrace
valgrind --leak-check=full ./prog
📖 Theory Focus
Debugging is a reasoning discipline: first classify the failure as syntax, compile-time, link-time, runtime, or logic-level.
Warnings matter because they expose mismatches between what you wrote and what the compiler can prove safely.
Tools such as GDB and Valgrind are effective only when you already have a hypothesis about state, control flow, or memory ownership.
⚠️ 11.1 – Types of Errors
Error Type
When Detected
Tool
Example
Syntax Error
Compile time
GCC
Missing ;, mismatched {}
Semantic Error
Compile time (warnings)
GCC -Wall
Using uninitialised variable
Linker Error
Link time
GCC/LD
Undefined reference to function
Runtime Error
While running
GDB, Valgrind
Segmentation fault, divide by 0
Logic Error
Testing/review
GDB print, unit tests
Wrong formula, off-by-one loop
🛠️ 11.2 – GCC Compilation Flags
Always compile with warnings enabled. Fix every warning — they often reveal real bugs.
# Comprehensive build flags for development
gcc -std=c11 -Wall -Wextra -Wpedantic -Wshadow \
-Wformat=2 -Wconversion -g \
-o program program.c
# Flag meanings:# -std=c11 Use C11 standard# -Wall Enable most warnings# -Wextra Extra warnings beyond -Wall# -Wpedantic Strict standard compliance# -Wshadow Warn when var shadows outer scope# -Wformat=2 Strict printf/scanf format checks# -Wconversion Implicit type conversions# -g Include debug symbols (for GDB)# For release builds (optimised, no debug info):
gcc -std=c11 -O2 -o program program.c
Common GCC Warnings and Fixes
Warning / Error
Cause
Fix
unused variable 'x'
Declared but never used
Remove it or use it
control reaches end of non-void function
Missing return statement
Add return value;
implicit declaration of function
Called before prototype/include
Add prototype or include header
comparison between signed and unsigned
Mixing int and size_t/unsigned
Cast explicitly: (int)sizeof(...)
format '%d' expects int, got float
Wrong printf format specifier
Use %f for float/double
undefined reference to 'sqrt'
Math function, missing -lm
Compile with -lm flag
Segmentation fault
Null/out-of-bounds dereference
Use GDB + Valgrind to pinpoint
🔬 11.3 – GDB Debugger Workflow
Compile
gcc -g -o prog prog.c
→
Start GDB
gdb ./prog
→
Set Breakpoint
break main break file.c:25
→
Run / Step
run, next, step continue
→
Inspect
print x info locals backtrace
# ─── GDB Quick Reference ───────────────────────────
$ gcc -g -o prog prog.c # compile with debug symbols
$ gdb ./prog # start debugger# Inside GDB:
(gdb) break main # breakpoint at start of main
(gdb) break prog.c:42 # breakpoint at line 42
(gdb) run # start execution
(gdb) run arg1 arg2 # run with arguments
(gdb) next # step over (don't enter functions)
(gdb) step # step into function calls
(gdb) continue # run until next breakpoint
(gdb) finish # run until current function returns
(gdb) print x # print value of variable x
(gdb) print *ptr # dereference and print
(gdb) print arr[0]@5 # print 5 elements starting at arr[0]
(gdb) info locals # all local variables
(gdb) info breakpoints # list all breakpoints
(gdb) backtrace # bt: call stack at crash point
(gdb) list # show source code around current line
(gdb) watch x # stop when x changes
(gdb) delete 1 # remove breakpoint 1
(gdb) quit # exit GDB
🧹 11.4 – Valgrind: Memory Error Detector
# Run program under Valgrind (Linux/WSL)
$ valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes ./prog
# Valgrind detects:# • Memory leaks (forgot to free)# • Use after free (dangling pointer)# • Buffer overruns (array out of bounds)# • Use of uninitialised values# • Double free# Example report excerpt:==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
==12345== ERROR SUMMARY: 1 errors from 1 contexts
✅ 11.5 – C Programming Best Practices
/* ── 1. Always initialise variables ── */int x = 0; // not just: int x;int *p = NULL; // not just: int *p;/* ── 2. Check return values ── */
FILE *fp = fopen("f.txt", "r");
if (!fp) { perror("fopen"); return1; } // always check!/* ── 3. Use constants instead of magic numbers ── */#define MAX_SIZE 100#define BUFFER_LEN 256/* ── 4. Validate all input at the boundary ── */if (index < 0 || index >= MAX_SIZE) {
fprintf(stderr, "Error: index %d out of range\n", index);
return-1;
}
/* ── 5. Use const for read-only parameters ── */voidprintName(constchar *name) { ... }
/* ── 6. Use sizeof(type) for portability ── */int *arr = malloc(10 * sizeof(int)); // not * 4/* ── 7. Avoid global variables when possible ── *//* ── 8. Free memory and close handles ── */free(ptr); ptr = NULL;
fclose(fp); fp = NULL;
/* ── 9. Break long functions into smaller ones ── *//* ── 10. Meaningful variable names ── */// Bad: int a, b, c; tmp1, tmp2// Good: int studentCount, totalScore;
Unit 15 – Lab Exercises
Write a program with an intentional segfault (null dereference). Compile with -g, run under GDB, use backtrace to locate the crash. Fix it.
Write a program with a memory leak (malloc without free). Run Valgrind and observe the leak report. Fix the leak.
Create a program with an off-by-one array access. Compile with -Wall -Wextra, address-sanitize with -fsanitize=address. Fix it.
Debug this broken binary search implementation (wrong mid calculation, wrong termination) using GDB breakpoints and variable inspection.
Set up a Makefile with targets: all (compile with -Wall -g), release (compile with -O2), clean (remove .o and binary), valgrind (run under Valgrind).
Hint: Address Sanitizer: gcc -fsanitize=address,undefined -g -o prog prog.c — detects buffer overflows and UB at runtime.
Unit 16 – Mini Projects
Integrate everything from this course into real-world programs. Each project applies multiple concepts: structs, file I/O, functions, pointers, and proper error handling.
🧪 Project Skeleton Syntax
Mini projects combine earlier syntax patterns into a repeatable structure: type definitions, helper functions, a validated menu loop, and a clean build command.
typedefstruct { int id; char name[40]; } Record;
voidaddRecord(void);
voidlistRecords(void);
intmain(void) {
int choice;
do { /* read choice, switch, call helpers */ } while (choice != 0);
}
📖 Theory Focus
Projects are where syntax becomes design: you stop learning isolated rules and start coordinating data, control flow, persistence, and validation.
A small project should still have module boundaries, reusable functions, predictable data models, and testable behaviour.
The goal is not just to make a demo run once; it is to build a program that survives invalid input, repeated use, and future extension.
#include<stdio.h>#include<string.h>#include<ctype.h>#define MAX_WORDS 1000#define WORD_LEN 64typedefstruct { char word[WORD_LEN]; int count; } WordEntry;
WordEntry table[MAX_WORDS];
int tableSize = 0;
/* Convert to lowercase in-place */voidtoLower(char *s) {
for(int i=0; s[i]; i++) s[i] = (char)tolower((unsigned char)s[i]);
}
/* Add word to frequency table */voidaddWord(constchar *w) {
for(int i=0; i<tableSize; i++) {
if (strcmp(table[i].word, w)==0) { table[i].count++; return; }
}
if (tableSize < MAX_WORDS) {
strncpy(table[tableSize].word, w, WORD_LEN-1);
table[tableSize].count = 1;
tableSize++;
}
}
/* Sort by frequency descending (bubble sort) */voidsortByFreq() {
for(int i=0; i<tableSize-1; i++)
for(int j=0; j<tableSize-i-1; j++)
if (table[j].count < table[j+1].count) {
WordEntry tmp = table[j]; table[j]=table[j+1]; table[j+1]=tmp;
}
}
intmain(int argc, char **argv) {
if (argc < 2) { printf("Usage: ./wordfreq <file>\n"); return1; }
FILE *fp = fopen(*(argv + 1), "r");
if (!fp) { perror("fopen"); return1; }
char w[WORD_LEN];
while (fscanf(fp, "%63s", w) == 1) {
/* strip punctuation from end */int len = (int)strlen(w);
while (len > 0 && !isalnum((unsigned char)w[len-1])) w[--len]='\0';
if (len > 0) { toLower(w); addWord(w); }
}
fclose(fp);
sortByFreq();
printf("Top words in %s:\n", argv[1]);
int limit = tableSize < 20 ? tableSize : 20;
for(int i=0; i<limit; i++)
printf("%4d %s\n", table[i].count, table[i].word);
return0;
}
wordfreq.c – compile: gcc -Wall -o wordfreq wordfreq.c && ./wordfreq myfile.txt
🎓 Capstone Challenge Ideas
Bank Account System — structs, linked list, file persistence, menu-driven
Sorting Visualizer — implement bubble/selection/insertion/merge sort with step-by-step print output; time with clock()
Simple Shell — read commands with fgets, tokenize with strtok, execute with execvp, fork processes
Matrix Calculator — add, sub, mul, transpose, determinant for n×n matrices using dynamic allocation
Text Editor (Vim-lite) — read/write files, line-based editing, search/replace using C string functions
Unit 16 – Submission Checklist
✅ Program compiles with gcc -Wall -Wextra -std=c11 and zero warnings
✅ All malloc calls are paired with free; Valgrind shows 0 leaks
✅ All file operations check for NULL FILE*
✅ No buffer overflows: all string reads are bounded (%49s, fgets)
✅ Functions are small; each does ONE thing well
✅ Code has meaningful variable/function names
✅ Tested with edge cases: empty input, very large input, invalid input
✅ Submitted with a Makefile
Unit 11 – Functions with Pointers
Functions and pointers are the two most powerful features of C. When combined, they unlock pass-by-reference, multiple return values, callbacks, and function dispatch tables. This unit covers everything from pointer parameters to advanced function pointers, with diagrams, working code, and 10 practical assignments.
🔁 Pointer Function Syntax
This topic extends normal function syntax by making parameters and even the functions themselves addressable values.
voidswap(int *a, int *b);
int (*op)(int, int) = add;
voidswap(int *a, int *b) {
int temp = *a; *a = *b; *b = temp;
}
result = op(3, 4);
swap(&x, &y);
📖 Theory Focus
Pointer parameters are how C expresses mutation, out-parameters, and efficient passing of large objects.
Function pointers turn behaviour into data, which enables callback APIs, lookup tables, event dispatch, and configurable algorithms.
This topic matters because it bridges basic procedural programming and systems-style API design where data and behaviour are both passed around explicitly.
🧠 13.1 – How Function Calls Work: Value vs Pointer
When you call a function in C, arguments are copied onto the stack. Changes inside the function do not affect the original variable — unless you pass a pointer to it.
PASS BY VALUE
int x = 10;
double(x); // passes COPYprintf("%d", x); // still 10!voiddouble(int n) {
n *= 2; // local copy only
}
PASS BY POINTER
int x = 10;
doubleIt(&x); // passes ADDRESSprintf("%d", x); // now 20 ✓voiddoubleIt(int *n) {
*n *= 2; // dereference → actual x
}
Memory layout during doubleIt(&x):
caller's stack frame
x
20
0x1004
→
doubleIt() stack frame
n (ptr)
0x1004
points to x
*n=2×(*n)
effect on caller
x
20
CHANGED ✓
#include<stdio.h>/* By value – caller unchanged */voidbyValue(int n) {
n = n * 2;
printf(" inside byValue: n=%d\n", n);
}
/* By pointer – modifies caller's variable */voidbyPointer(int *p) {
*p = *p * 2;
printf(" inside byPointer: *p=%d\n", *p);
}
intmain() {
int a = 5, b = 5;
printf("--- Pass by Value ---\n");
byValue(a);
printf(" caller a = %d (unchanged)\n", a); // 5printf("--- Pass by Pointer ---\n");
byPointer(&b);
printf(" caller b = %d (CHANGED)\n", b); // 10return0;
}
value_vs_pointer.c
🔄 13.2 – Passing Pointers to Functions
Pointer parameters are the standard C technique for: (1) modifying caller variables, (2) returning multiple values from one function, and (3) efficiently passing large structs without copying them.
#include<stdio.h>/* ── Classic swap using pointers ── */voidswap(int *a, int *b) {
int temp = *a; // save value at address a
*a = *b; // write value of b into a's location
*b = temp; // write saved a into b's location
}
/* ── Multiple return values via pointers ── */voidminMax(int *arr, int n, int *min, int *max) {
*min = *max = *(arr + 0);
for (int i = 1; i < n; i++) {
if (*(arr + i) < *min) *min = *(arr + i);
if (*(arr + i) > *max) *max = *(arr + i);
}
}
/* ── Divmod: quotient + remainder together ── */voiddivmod(int a, int b, int *quot, int *rem) {
*quot = a / b;
*rem = a % b;
}
/* ── Passing large struct by pointer (no copy) ── */typedefstruct { char name[50]; int score; double gpa; } Student;
voidprintStudent(const Student *s) { // const: read-only pointerprintf("Name: %s Score: %d GPA: %.2f\n", s->name, s->score, s->gpa);
}
intmain() {
/* Swap */int x = 10, y = 20;
printf("Before swap: x=%d y=%d\n", x, y);
swap(&x, &y);
printf("After swap: x=%d y=%d\n", x, y); // x=20 y=10/* Min/Max */int data[] = {34, 7, 89, 12, 55};
int lo, hi;
minMax(data, 5, &lo, &hi);
printf("Min=%d Max=%d\n", lo, hi); // 7 89/* Divmod */int q, r;
divmod(17, 5, &q, &r);
printf("17 / 5 = %d remainder %d\n", q, r); // 3 r 2/* Struct by pointer */
Student s = {"Alice", 92, 3.8};
printStudent(&s);
return0;
}
pointer_params.c
Use const T *p when you pass a pointer for reading only. This prevents accidental modification and communicates intent clearly to other programmers.
📋 13.3 – Arrays as Pointer Parameters
In C, an array name decays to a pointer to its first element when passed to a function. The function receives a pointer, not a copy of the array, so it can modify the original array's elements.
#include<stdio.h>/* Both declarations are IDENTICAL — array notation is syntactic sugar */voidfillArray(int *arr, int n); // pointer stylevoidprintArray(int *arr, int n); // pointer stylevoidreverseArray(int *arr, int n);
intsumArray(constint *arr, int n); // const: won't modifyvoidfillArray(int *arr, int n) {
for (int i = 0; i < n; i++)
arr[i] = (i + 1) * 10; // arr[i] identical to *(arr+i)
}
voidprintArray(int *arr, int n) {
for (int i = 0; i < n; i++)
printf("%d ", *(arr + i));
printf("\n");
}
voidreverseArray(int *arr, int n) {
int *lo = arr, *hi = arr + n - 1; // pointer-based two-pointer techniquewhile (lo < hi) {
int tmp = *lo; *lo = *hi; *hi = tmp;
lo++; hi--;
}
}
intsumArray(constint *arr, int n) {
int sum = 0;
// Pointer arithmetic traversal (no index variable needed)for (constint *p = arr; p < arr + n; p++)
sum += *p;
return sum;
}
intmain() {
int a[5];
fillArray(a, 5);
printf("Filled : "); printArray(a, 5); // 10 20 30 40 50printf("Sum : %d\n", sumArray(a, 5)); // 150reverseArray(a, 5);
printf("Reversed: "); printArray(a, 5); // 50 40 30 20 10return0;
}
array_pointers.c
sizeof(arr) inside a function gives the size of the pointer, not the array. Always pass the array length as a separate parameter.
↩️ 13.4 – Returning Pointers from Functions
A function can return a pointer — but you must ensure the memory it points to is still valid after the function returns. There are three safe strategies and one common fatal mistake.
#include<stdio.h>#include<stdlib.h>#include<string.h>/* ══ SAFE 1: Return pointer to STATIC variable ══
static variables live for entire program lifetime */constchar* getStatus(int code) {
staticconstchar *msgs[] = {"OK", "Error", "Timeout"};
if (code < 0 || code > 2) return"Unknown";
return msgs[code]; // safe: static storage
}
/* ══ SAFE 2: Return pointer to caller-provided buffer ══ */char* buildGreeting(constchar *name, char *buf, int bufLen) {
snprintf(buf, bufLen, "Hello, %s!", name);
return buf; // buf lives in caller's frame — safe
}
/* ══ SAFE 3: Return heap-allocated memory ══
Caller MUST call free() when done */int* makeRange(int start, int end) {
int n = end - start + 1;
int *arr = (int*)malloc(n * sizeof(int));
if (!arr) returnNULL;
for (int i = 0; i < n; i++) arr[i] = start + i;
return arr; // caller must free(arr)
}
/* ══ DANGER: Return pointer to LOCAL variable ══
LOCAL variables are destroyed when function returns!
This creates a DANGLING POINTER — undefined behavior! */int* dangerousFunc() {
int local = 42;
return &local; // ⚠️ NEVER DO THIS! local no longer exists after return
}
intmain() {
/* Static */printf("Status 0: %s\n", getStatus(0)); // OKprintf("Status 1: %s\n", getStatus(1)); // Error/* Caller buffer */char greeting[64];
printf("%s\n", buildGreeting("Alice", greeting, sizeof(greeting)));
/* Heap */int *range = makeRange(1, 5);
if (range) {
for (int i = 0; i < 5; i++) printf("%d ", range[i]);
printf("\n");
free(range); // MUST free!
}
return0;
}
returning_pointers.c
Dangling Pointer: Never return a pointer to a local variable. Once the function returns, the local variable's stack memory is reclaimed. Accessing it is undefined behavior — your program may crash, produce garbage, or appear to work (until it doesn't).
📌 13.5 – Function Pointers
A function pointer stores the address of a function. It allows you to select and call different functions at runtime — enabling callbacks, plugin systems, and dispatch tables.
Anatomy of a function pointer declaration:
int (*funcPtr)(int, int);
▲ return type
▲ pointer name (note the *)
▲ parameter types
#include<stdio.h>/* Four functions with the same signature: int(int, int) */intadd(int a, int b) { return a + b; }
intsub(int a, int b) { return a - b; }
intmul(int a, int b) { return a * b; }
intdivSafe(int a, int b) { return b ? a / b : 0; }
/* typedef for cleaner syntax */typedefint (*BinaryOp)(int, int);
/* Dispatch table: array of function pointers */
BinaryOp ops[] = { add, sub, mul, divSafe };
constchar *opNames[] = { "add", "sub", "mul", "div" };
intmain() {
/* ── Direct function pointer usage ── */
BinaryOp fp = add; // assign function addressprintf("add(3,4) = %d\n", fp(3, 4)); // call via pointer: 7
fp = mul;
printf("mul(3,4) = %d\n", fp(3, 4)); // 12/* ── Dispatch table: select operation at runtime ── */printf("\nDispatch table (a=10, b=3):\n");
for (int i = 0; i < 4; i++)
printf(" %s(10,3) = %d\n", opNames[i], ops[i](10, 3));
/* ── NULL check before calling ── */
BinaryOp op = NULL;
if (op != NULL)
op(1, 2);
elseprintf("\nFunction pointer is NULL — safe check passed\n");
/* ── Explicit dereference syntax (both work) ── */printf("Direct call : %d\n", (*add)(5, 6)); // old styleprintf("Modern call : %d\n", add(5, 6)); // preferredreturn0;
}
function_pointers.c
Syntax
Meaning
int (*fp)(int, int)
Declare function pointer fp
fp = add
Assign: fp now points to add
fp(3, 4)
Call the function through fp
typedef int (*BinaryOp)(int,int)
Create type alias for cleaner code
BinaryOp ops[] = {add, sub}
Array of function pointers (dispatch table)
🔁 13.6 – Callbacks (Functions as Arguments)
A callback is a function pointer passed as an argument to another function. The receiving function calls it at the right time. This is the foundation of event systems, sorting, and generic algorithms.
#include<stdio.h>#include<stdlib.h>/* ── Custom forEach: applies callback to every element ── */voidforEach(int *arr, int n, void (*callback)(int)) {
for (int i = 0; i < n; i++)
callback(arr[i]);
}
/* ── Custom map: transform each element using a function ── */voidmapArray(int *arr, int n, int (*transform)(int)) {
for (int i = 0; i < n; i++)
arr[i] = transform(arr[i]);
}
/* Callback functions to pass */voidprintOne(int x) { printf("%d ", x); }
intsquare(int x) { return x * x; }
intnegate(int x) { return -x; }
/* ── qsort from <stdlib.h> — the standard library callback ── */intascending(constvoid *a, constvoid *b) {
return (*(int*)a) - (*(int*)b); // < 0 if a < b
}
intdescending(constvoid *a, constvoid *b) {
return (*(int*)b) - (*(int*)a);
}
intmain() {
int data[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(data) / sizeof(data[0]);
/* forEach with printOne */printf("Original: "); forEach(data, n, printOne); printf("\n");
/* map: square all elements */mapArray(data, n, square);
printf("Squared : "); forEach(data, n, printOne); printf("\n");
/* qsort ascending */int nums[] = {64, 25, 12, 90, 5};
qsort(nums, 5, sizeof(int), ascending);
printf("qsort ASC: "); forEach(nums, 5, printOne); printf("\n");
/* qsort descending — just swap the comparator callback! */qsort(nums, 5, sizeof(int), descending);
printf("qsort DESC: "); forEach(nums, 5, printOne); printf("\n");
return0;
}
callbacks.c
qsort signature:void qsort(void *base, size_t n, size_t size, int (*cmp)(const void*, const void*)). Your comparator must return negative, zero, or positive — not just 0/1.
🔗 13.7 – Double Pointers (Pointer to Pointer)
A double pointer (int **pp) is a pointer that holds the address of another pointer. It is needed when a function must modify a pointer in the caller's scope — for example, allocating a list or changing where a pointer points.
Memory chain: int **pp → int *p → int value
pp
addr of p
int **
→
p
addr of val
int *
→
val
42
int
*pp → dereferences to p | **pp → dereferences to 42
#include<stdio.h>#include<stdlib.h>#include<string.h>/* ── Use case 1: allocate inside a function ── */voidallocate(int **pp, int value) {
*pp = (int*)malloc(sizeof(int)); // write new address into caller's pointerif (*pp) **pp = value;
}
/* ── Use case 2: reassign a pointer in the caller ── */voidresetToNull(int **pp) {
free(*pp);
*pp = NULL; // set caller's pointer to NULL after freeing
}
/* ── Use case 3: argv is char** (array of char pointers) ── */voidprintArgs(int argc, char **argv) {
for (int i = 0; i < argc; i++)
printf(" argv[%d] = %s\n", i, argv[i]);
}
/* ── Use case 4: linked list node insertion via ** ── */typedefstruct Node { int val; struct Node *next; } Node;
voidprepend(Node **head, int val) {
Node *n = (Node*)malloc(sizeof(Node));
n->val = val;
n->next = *head; // new node points to old head
*head = n; // update caller's head pointer
}
intmain() {
/* Allocate */int *p = NULL;
allocate(&p, 42);
if (p) { printf("Allocated: *p = %d\n", *p); }
resetToNull(&p);
printf("After reset: p = %s\n", p ? "non-NULL" : "NULL");
/* Linked list via ** */
Node *head = NULL;
prepend(&head, 30);
prepend(&head, 20);
prepend(&head, 10);
printf("List: ");
for (Node *cur = head; cur; cur = cur->next)
printf("%d ", cur->val); // 10 20 30printf("\n");
/* Free list */while (head) { Node *tmp = head; head = head->next; free(tmp); }
return0;
}
double_pointers.c
🔒 13.8 – const with Pointer Parameters
The const keyword combined with pointers has four combinations. Understanding them helps you write safer APIs where functions clearly document whether they read or modify their arguments.
Declaration
Pointer changeable?
Value changeable?
Use case
int *p
Yes
Yes
General in/out parameter
const int *p
Yes
No
Read-only access to data
int * const p
No
Yes
Fixed address, modifiable value
const int * const p
No
No
Fully immutable
#include<stdio.h>/* Read-only: cannot change *p, can change p itself */voidprintValue(constint *p) {
printf("Value: %d\n", *p);
// *p = 99; ← COMPILE ERROR: read-only// p = NULL; ← OK: pointer itself can change (local copy)
}
/* Fixed pointer: cannot reassign p, but *p is modifiable */voidincrement(int * const p) {
(*p)++; // OK: modifying the value// p = NULL; ← COMPILE ERROR: pointer is const
}
/* Fully const: ideal for string processing functions */intcountChar(constchar * const s, char c) {
int count = 0;
for (int i = 0; s[i] != '\0'; i++)
if (s[i] == c) count++;
return count;
}
intmain() {
int x = 10;
printValue(&x); // Value: 10increment(&x); // x becomes 11printValue(&x); // Value: 11printf("'l' count in 'hello': %d\n", countChar("hello", 'l')); // 2/* const pointer to int (fixed address) */int val = 5;
int * const fixed = &val;
*fixed = 99; // OK — value changedprintf("val = %d\n", val); // 99return0;
}
const_pointers.c
Memory trick: Read the declaration right-to-left. const int *p → "p is a pointer to an int that is const". int * const p → "p is a const pointer to an int".
This final example combines function pointers, callbacks, pointer parameters, and const to build a small but complete generic utility library — the kind of code found in real C systems.
#include<stdio.h>#include<stdlib.h>#include<string.h>/* ─── Types ─────────────────────────────────────── */typedefint (*Comparator)(constvoid*, constvoid*);
typedefvoid (*Printer)(constvoid*);
typedefstruct {
char name[32];
int age;
double gpa;
} Student;
/* ─── Comparators ────────────────────────────────── */intcmpByName(constvoid *a, constvoid *b) {
returnstrcmp(((const Student*)a)->name,
((const Student*)b)->name);
}
intcmpByAge(constvoid *a, constvoid *b) {
return ((const Student*)a)->age - ((const Student*)b)->age;
}
intcmpByGPA(constvoid *a, constvoid *b) {
double diff = ((const Student*)b)->gpa - ((const Student*)a)->gpa;
return (diff > 0) - (diff < 0); // safe double comparison
}
/* ─── Printer ────────────────────────────────────── */voidprintStudent(constvoid *p) {
const Student *s = (const Student*)p;
printf(" %-12s age=%2d gpa=%.1f\n", s->name, s->age, s->gpa);
}
/* ─── Generic linear search (returns index or -1) ── */intlinearSearch(constvoid *base, int n, size_t elemSize,
constvoid *target, Comparator cmp) {
for (int i = 0; i < n; i++) {
constchar *elem = (constchar*)base + i * elemSize;
if (cmp(elem, target) == 0) return i;
}
return -1;
}
/* ─── Print all with header ─────────────────────── */voidprintAll(constvoid *base, int n, size_t elemSize, Printer pr) {
for (int i = 0; i < n; i++)
pr((constchar*)base + i * elemSize);
}
intmain() {
Student students[] = {
{"Charlie", 22, 3.5},
{"Alice", 20, 3.9},
{"Bob", 21, 3.2},
{"Diana", 23, 3.8},
};
int n = sizeof(students) / sizeof(students[0]);
/* Sort by name */qsort(students, n, sizeof(Student), cmpByName);
printf("Sorted by Name:\n");
printAll(students, n, sizeof(Student), printStudent);
/* Sort by GPA (descending) */qsort(students, n, sizeof(Student), cmpByGPA);
printf("Sorted by GPA (highest first):\n");
printAll(students, n, sizeof(Student), printStudent);
/* Linear search by name */
Student target = {"Bob", 0, 0.0};
int idx = linearSearch(students, n, sizeof(Student), &target, cmpByName);
printf("Search 'Bob': index %d\n", idx);
return0;
}
generic_sort_search.c
📝 Unit 11 – 10 Assignments: Functions with Pointers
Swap Three Values: Write a function void swap3(int *a, int *b, int *c) that rotates three values left: after the call, a gets b's value, b gets c's value, and c gets a's original value. Call it in main and print before and after. Demonstrate that pass-by-value would not work here.
Multiple Return Values: Write a function void statistics(int *arr, int n, int *min, int *max, double *avg) that computes the minimum, maximum, and average of an array in one pass. Return all three values via pointer parameters. Test with at least two different arrays and print all three statistics for each.
Array Operations Library: Write four functions — all accepting int *arr and int n:
(a) void scaleArray(int *arr, int n, int factor) — multiply every element by factor
(b) void shiftArray(int *arr, int n, int offset) — add offset to every element
(c) int countPositive(const int *arr, int n) — count elements > 0
(d) void copyArray(const int *src, int *dst, int n) — copy src to dst
In main, test all four on the same array, printing results at each stage.
Dangling Pointer Demo: Write a function that returns a pointer to a local variable. Compile with gcc -Wall and note the warning. In a comment, explain what happens in memory when the function returns. Then rewrite the function correctly using static storage or a caller-provided buffer, and verify the fix works.
Function Pointer Calculator: Define five functions: add, subtract, multiply, safeDivide, modulo — all with signature int(int,int). Store them in an array of function pointers. Write a menu-driven calculator loop: read two integers and an operator choice (1–5), look up the correct function pointer in the array, and call it. Continue until the user enters 0 to quit.
Custom qsort Comparators: Declare an array of 8 structs: typedef struct { char name[32]; int score; } Player; Write three comparators: by name (alphabetical), by score ascending, by score descending. Sort and print the array three times using qsort with each comparator. The output should show three different orderings.
Callback forEach and map: Implement two higher-order functions:
(a) void forEach(int *arr, int n, void (*action)(int*)) — calls action on a pointer to each element (allows modification)
(b) int reduce(const int *arr, int n, int init, int (*combiner)(int, int)) — folds the array with a combiner function
Write callback functions doubleInPlace, addOne, sumTwo, maxTwo. Use them to double an array, increment every element, compute total sum, and find the maximum.
Double Pointer Allocation: Write a function void allocMatrix(int ***mat, int rows, int cols) that allocates a 2D matrix using a double pointer (int ** for the matrix). Write a companion void freeMatrix(int ***mat, int rows). In main, allocate a 3×3 matrix, fill it with row*10+col, print it as a grid, then free it and set the pointer to NULL.
Linked List with Double Pointers: Implement a singly linked list using Node **head as the parameter type for all modifying operations. Write:
(a) void pushFront(Node **head, int val) (b) void pushBack(Node **head, int val) (c) int popFront(Node **head) — removes and returns front value
(d) void printList(const Node *head) Build a list of 5 elements, print it, pop 2 from front, push 3 to back, print again. Free all nodes.
const Correctness Audit: Write a struct typedef struct { char title[64]; int pages; double price; } Book; and four functions:
(a) void printBook(const Book *b) — read-only
(b) void applyDiscount(Book *b, double pct) — modifies price
(c) void copyBook(const Book *src, Book *dst) — reads src, writes dst
(d) int comparePrice(const Book *a, const Book *b) — returns -1, 0, 1
Ensure every pointer parameter uses the correct const qualification. Try removing a const and verifying that the compiler produces an error when you attempt to modify a const-qualified value.